From d0806d741e9b8b8065c98e572be8ddf5bbff7e25 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 18:59:48 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Mikachu2333=20?= =?UTF-8?q?=E7=9A=84=E9=82=AE=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/PR-test.yml | 4 ++-- ...45\246\202\344\275\225\347\274\226\345\206\231recipe.md" | 5 +++-- lib/xy.h | 6 +++--- src/chsrc-main.c | 2 +- src/recipe/lang/Python/uv.c | 2 +- src/recipe/recipe-template.c | 4 ++-- test/xy.c | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/PR-test.yml b/.github/workflows/PR-test.yml index 7ffa5bf8..9ec3ac04 100644 --- a/.github/workflows/PR-test.yml +++ b/.github/workflows/PR-test.yml @@ -1,7 +1,7 @@ # --------------------------------------------------------------- # Workflow File : PR-test.yml -# File Authors : 曾奥然 -# Contributors : Mikachu2333 +# File Authors : 曾奥然 +# Contributors : Mikachu2333 # | # Created On : <2025-06-19> # Last Modified : <2025-08-17> diff --git "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" index c35aec10..c45bd716 100644 --- "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" +++ "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" @@ -3,8 +3,9 @@ ! ------------------------------------------------------------- ! Doc Type : Markdown ! Doc Name : 10-如何编写recipe.md - ! Doc Authors : 曾奥然 - ! Contributors : Nul None + ! Doc Authors : 曾奥然 + ! Contributors : Nul None + Mikachu2333 ! | ! Created On : <2024-08-19> ! Last Modified : <2026-01-21> diff --git a/lib/xy.h b/lib/xy.h index 717445aa..936edb92 100644 --- a/lib/xy.h +++ b/lib/xy.h @@ -2,9 +2,9 @@ * Copyright © 2023-2026 曾奥然, 郭恒 * SPDX-License-Identifier: MIT * ------------------------------------------------------------- - * Lib Authors : 曾奥然 - * | 郭恒 <2085471348@qq.com> - * Contributors : Mikachu2333 + * Lib Authors : 曾奥然 + * | 郭恒 <2085471348@qq.com> + * Contributors : Mikachu2333 * | juzeon * | BingChunMoLi * | AnonTokio diff --git a/src/chsrc-main.c b/src/chsrc-main.c index 964b65af..2b1ddc6e 100644 --- a/src/chsrc-main.c +++ b/src/chsrc-main.c @@ -69,7 +69,7 @@ chsrc_register_contributors () chef_register_contributor ("@Kattos", "ccy", "icuichengyi@gmail.com", NULL); chef_register_contributor ("@xrgzs", "MadDogOwner", "xiaoran@xrgzs.top", NULL); chef_register_contributor ("@sanchuanhehe", "sanchuanhehe", "wyihe5520@gmail.com", NULL); - chef_register_contributor ("@Mikachu2333", "Mikachu2333", "mikachu.23333@zohomail.com", NULL); + chef_register_contributor ("@Mikachu2333", "Mikachu2333", "linkchou@yandex.com", NULL); chef_register_contributor ("@techoc", "Rui Yang", "techoc@foxmail.com", NULL); chef_register_contributor ("@BingChunMoLi", "BingChunMoLi", "bingchunmoli@bingchunmoli.com", NULL); chef_register_contributor ("@wcbing", "wcbing", "i@wcbing.top", NULL); diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c index df41ee7f..16c02bb5 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv.c @@ -10,7 +10,7 @@ pl_python_uv_prelude (void) chef_prep_this (pl_python_uv, gsr); chef_set_recipe_created_on (this, "2024-12-11"); - chef_set_recipe_last_updated (this, "2025-12-29"); + chef_set_recipe_last_updated (this, "2026-08-02"); chef_set_chefs (this, 2, "@happy-game", "@MingriLingran"); chef_set_sauciers (this, 2, "@Kattos", "@ccmywish"); diff --git a/src/recipe/recipe-template.c b/src/recipe/recipe-template.c index c25e96ca..9dfe75e2 100644 --- a/src/recipe/recipe-template.c +++ b/src/recipe/recipe-template.c @@ -2,8 +2,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later * ------------------------------------------------------------- * File Name : recipe-template.c - * File Authors : 曾奥然 - * Contributors : Mikachu2333 + * File Authors : 曾奥然 + * Contributors : Mikachu2333 * | * Created On : <2024-08-09> * Last Modified : <2026-08-02> diff --git a/test/xy.c b/test/xy.c index 34de9329..9db5f70a 100644 --- a/test/xy.c +++ b/test/xy.c @@ -2,8 +2,8 @@ * SPDX-License-Identifier: MIT * ------------------------------------------------------------- * Test File : xy.c - * Test Authors : 曾奥然 - * Contributors : Mikachu2333 + * Test Authors : 曾奥然 + * Contributors : Mikachu2333 * Created On : <2023-08-30> * Last Modified : <2026-03-22> * From 0fa9e340416c4ef0332fa0ffd70c61c8f1935edc Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 19:04:35 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E6=8E=92=E9=99=A4clangd=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E7=9A=84=E5=B9=B2=E6=89=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 89397131..6047583c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,4 +24,9 @@ }, "vscode-just.formatOnSave": false, + + "clangd.enable": false, + "clangd.enableCodeCompletion": false, + "clangd.enableHover": false, + "clangd.detectExtensionConflicts": false } From 8ad3c02f8bb2d6171af8c2433dbc0e397aabd7a3 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 19:45:53 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E9=87=8D=E6=9E=84=20uv=20=E6=BA=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=86=99=E5=85=A5=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20uv-helper=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 uv-helper.c,提供 uv.toml 配置字符串改写工具 - 支持替换/插入 [[index]] 段的 url 与 default 行 - 支持替换/插入/删除顶层 python-install-mirror 键 - set 时同时配置 PyPI 索引源与 Python 下载源 - reset 时清理 [[index]] 的 url/default 与顶层 python-install-mirror - get 时读取并输出 python-install-mirror 配置 - 新增 uv-helper 单元测试并接入 make/just 测试流程 1 --- Makefile | 7 +- justfile | 8 +- src/recipe/lang/Python/uv-helper.c | 305 +++++++++++++++++++++++++++++ src/recipe/lang/Python/uv.c | 208 ++++++++++++++------ test/uv-helper-test.c | 264 +++++++++++++++++++++++++ 5 files changed, 729 insertions(+), 63 deletions(-) create mode 100644 src/recipe/lang/Python/uv-helper.c create mode 100644 test/uv-helper-test.c diff --git a/Makefile b/Makefile index 6943fbea..9915410f 100755 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ build-in-ci-release-mode: debug: build-in-debug-mode @$(DEBUGGER) $(DebugMode-Target-Name) -test: test-make-env test-xy test-fw +test: test-make-env test-xy test-fw test-uv-helper test-make-env: @echo "On-Linux: $(On-Linux)" @@ -193,6 +193,10 @@ test-fw: @$(CC) test/fw.c $(CFLAGS) -o fw @./fw +test-uv-helper: + @$(CC) test/uv-helper-test.c $(CFLAGS) -o uv-helper-test + @./uv-helper-test + check: test # AUR package 安装时将执行此 target @@ -207,6 +211,7 @@ clean: -@rm *.res 2>/dev/null -@rm xy 2>/dev/null -@rm fw 2>/dev/null + -@rm uv-helper-test 2>/dev/null -@rm README.md.bak* 2>/dev/null -@rm chsrc 2>/dev/null diff --git a/justfile b/justfile index a7e4dc73..7eb677d5 100644 --- a/justfile +++ b/justfile @@ -93,6 +93,7 @@ CFLAGS_release_mode := CFLAGS_release_mode_prompt + ' ' + CFLAGS_warn BIN_xy := if os() == 'windows' {'xy.exe'} else {'./xy'} BIN_fw := if os() == 'windows' {'fw.exe'} else {'./fw'} +BIN_uv-helper-test := if os() == 'windows' {'uv-helper-test.exe'} else {'./uv-helper-test'} BIN_rm := if os() == 'windows' {'del'} else {'rm'} #======================= alias b := build-in-dev-mode @@ -132,7 +133,7 @@ build-in-release-mode: debug: build-in-debug-mode @{{DEBUGGER}} {{DebugMode-Target-Name}} -test: test-xy test-fw +test: test-xy test-fw test-uv-helper # 这两个测试文件都用 DEBUG mode test-xy: @@ -143,6 +144,10 @@ test-fw: @{{CC}} test/fw.c {{CFLAGS_debug_mode}} -o fw @{{BIN_fw}} +test-uv-helper: + @{{CC}} test/uv-helper-test.c -o uv-helper-test + @{{BIN_uv-helper-test}} + fastcheck: @perl ./test/cli.pl fastcheck @@ -154,6 +159,7 @@ clean: -@{{BIN_rm}} *.res -@{{BIN_rm}} xy -@{{BIN_rm}} fw + -@{{BIN_rm}} uv-helper-test -@{{BIN_rm}} chsrc -@{{BIN_rm}} chsrc-debug -@{{BIN_rm}} chsrc-release diff --git a/src/recipe/lang/Python/uv-helper.c b/src/recipe/lang/Python/uv-helper.c new file mode 100644 index 00000000..592894cd --- /dev/null +++ b/src/recipe/lang/Python/uv-helper.c @@ -0,0 +1,305 @@ +/** ------------------------------------------------------------ + * SPDX-License-Identifier: GPL-3.0-or-later + * ------------------------------------------------------------- + * Lib Authors : Mikachu2333 + * Contributors : EXAMPLE + * | + * Created On : <2026-08-02> + * Last Modified : <2026-08-02> + * + * + * uv.toml 配置字符串改写工具 (由 uv.c #include, 可被 test 单独 #include) + * + * 仅处理 chsrc 关心的 uv 配置子集, 不是完整 TOML 解析器: + * - [[index]] 数组表: 替换/清理其 url、default 行 (保留段头与其他键) + * - 顶层 python-install-mirror 键: 替换/删除 + * + * 刻意不处理: 多行字符串、数组/内联表值、字符串转义 —— + * 因为所有改写都按整行“替换/删除”进行, 无需解析值本身。 + * 已知限制: 多行字符串 (""") 内若出现行首 '[' 会被误认为段头, 罕见可忽略。 + * + * 所有函数均为 static, 依赖调用方所在翻译单元提供 stdlib/string.h。 + * ------------------------------------------------------------*/ + +#include +#include +#include +#include + +/** + * 判断行首 (调用方必须保证 p 位于行首) 是否以 key 作为键开始。 + * 键名后必须紧跟 =、空白或行尾, 避免误匹配 urlx 之类前缀键。 + */ +static bool +uvh_key_prefix (const char *p, const char *key) +{ + size_t kl = strlen (key); + if (strncmp (p, key, kl) != 0) return false; + char c = p[kl]; + return c == '=' || c == ' ' || c == '\t' || c == '\n' || c == '\0'; +} + +/** + * 判断行首是否为 [[index]] 数组表头 (允许行尾注释, 如 "[[index]] # note")。 + */ +static bool +uvh_is_index_header (const char *p) +{ + if (strncmp (p, "[[index]]", 9) != 0) return false; + char c = p[9]; + return c == '\n' || c == '\0' || c == ' ' || c == '\t'; +} + +/* 跳过当前行 (含换行符), 返回下一行的行首 */ +static const char * +uvh_next_line (const char *p) +{ + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + return p; +} + +/** + * 从 start (段内第一行) 开始找下一个段头行 (行首 '[') 的位置。 + * 返回 NULL 表示直到文件结尾都没有新段头。 + */ +static const char * +uvh_find_section_end (const char *start) +{ + const char *p = start; + while (*p) + { + if (*p == '[') return p; + p = uvh_next_line (p); + } + return NULL; +} + + +/** + * 在 content 中找到第一个 [[index]] 段, 替换其 url = "..." 行 (或单引号值)。 + * 文件中没有 [[index]] 段时, 追加整个段到末尾。 + * + * 只修改第一个 [[index]] 段; 段内 url/default 键的语义由调用者 (set) 决定。 + * + * @return 新内容 (caller-free) + */ +static char * +replace_pypi_index_url (const char *content, const char *url) +{ + /* 找第一个行首 [[index]] 段头 (跳过注释等非段头匹配) */ + const char *p = content; + const char *index_header = NULL; + while (*p) + { + if (*p == '[' && uvh_is_index_header (p)) { index_header = p; break; } + p = uvh_next_line (p); + } + + /* 文件中尚无 [[index]] 段, 追加到末尾 */ + if (!index_header) + { + size_t len = strlen (content) + strlen (url) + 96; + char *ret = calloc (len, 1); + size_t clen = strlen (content); + if (clen > 0 && content[clen - 1] == '\n') + snprintf (ret, len, "%s[[index]]\nurl = \"%s\"\ndefault = true\n", content, url); + else if (clen > 0) + snprintf (ret, len, "%s\n[[index]]\nurl = \"%s\"\ndefault = true\n", content, url); + else + snprintf (ret, len, "[[index]]\nurl = \"%s\"\ndefault = true\n", url); + return ret; + } + + /* 段内第一行与段边界 (下一个段头或文件尾) */ + const char *first = uvh_next_line (index_header); + const char *end = uvh_find_section_end (first); + + /* 在当前 [[index]] 段内找 url 键行 */ + const char *url_line = NULL; + for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) + { + if (uvh_key_prefix (s, "url")) { url_line = s; break; } + } + + /* 段内没有 url 键: 在段内第一行之前插入 url 行 (及缺失的 default 行) */ + if (!url_line) + { + bool has_default = false; + for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) + { + if (uvh_key_prefix (s, "default")) { has_default = true; break; } + } + + size_t len = strlen (content) + strlen (url) + 64; + char *ret = calloc (len, 1); + size_t pos = 0; + pos += snprintf (ret + pos, len - pos, "%.*s", (int)(first - content), content); + pos += snprintf (ret + pos, len - pos, "url = \"%s\"\n", url); + if (!has_default) + pos += snprintf (ret + pos, len - pos, "default = true\n"); + strcpy (ret + pos, first); + return ret; + } + + /* 替换 url 行, 保留该行原有的换行符与后续内容 */ + const char *line_end = strchr (url_line, '\n'); + if (!line_end) line_end = content + strlen (content); + + size_t len = strlen (content) + strlen (url) + 32; + char *ret = calloc (len, 1); + size_t pos = 0; + pos += snprintf (ret + pos, len - pos, "%.*s", (int)(url_line - content), content); + pos += snprintf (ret + pos, len - pos, "url = \"%s\"", url); + strcpy (ret + pos, line_end); + return ret; +} + + +/** + * 替换顶层 python-install-mirror 键的值; 顶层无该键时, 追加到顶层区域末尾 + * (第一个段头之前), 保证新键仍是顶层键而非被归入某个表。 + * + * 段内 (非顶层) 的同名键不受影响 —— chsrc 只管理顶层键。 + * + * @return 新内容 (caller-free) + */ +static char * +replace_python_install_mirror (const char *content, const char *url) +{ + /* 只扫描顶层区域 (第一个段头之前) */ + const char *p = content; + const char *old_line = NULL; + const char *first_section = NULL; + while (*p) + { + if (*p == '[') + { + first_section = p; + break; + } + if (uvh_key_prefix (p, "python-install-mirror")) old_line = p; + p = uvh_next_line (p); + } + + size_t base = strlen (content) + strlen (url) + 64; + + /* 顶层已有旧行: 就地替换 */ + if (old_line) + { + const char *line_end = strchr (old_line, '\n'); + if (!line_end) line_end = content + strlen (content); + + char *ret = calloc (base, 1); + size_t pos = 0; + pos += snprintf (ret + pos, base - pos, "%.*s", (int)(old_line - content), content); + pos += snprintf (ret + pos, base - pos, "python-install-mirror = \"%s\"", url); + strcpy (ret + pos, line_end); + return ret; + } + + /* 顶层无旧行: 追加到顶层区域末尾 (无段头则文件末尾) */ + const char *insert_at = first_section ? first_section : content + strlen (content); + size_t head = insert_at - content; + + char *ret = calloc (base, 1); + size_t pos = 0; + pos += snprintf (ret + pos, base - pos, "%.*s", (int)head, content); + if (head > 0 && content[head - 1] != '\n') ret[pos++] = '\n'; + pos += snprintf (ret + pos, base - pos, "python-install-mirror = \"%s\"\n", url); + strcpy (ret + pos, content + head); + return ret; +} + + +/** + * reset 专用: 删除每个 [[index]] 段内的 url/default 行 (保留段头与 + * name 等其余字段), 删除顶层 python-install-mirror 行, 其余内容原样保留。 + * + * 段边界严格限定: 其他表 (如 [python]) 内同名的 url/default 键不受影响。 + * + * @return 新内容 (caller-free) + */ +static char * +cleanup_config_for_reset (const char *content) +{ + size_t est = strlen (content) + 128; + char *ret = calloc (est, 1); + size_t pos = 0; + + bool in_table = false; /* 是否已进入任何表 (用于限定 python-install-mirror 仅顶层) */ + bool in_index = false; /* 当前是否处于 [[index]] 段 (用于限定 url/default) */ + + const char *p = content; + while (*p) + { + bool skip = false; + + if (*p == '[') + { + in_table = true; + in_index = uvh_is_index_header (p); + } + else if (in_index && (uvh_key_prefix (p, "url") || uvh_key_prefix (p, "default"))) + { + skip = true; + } + else if (!in_table && uvh_key_prefix (p, "python-install-mirror")) + { + skip = true; + } + + if (skip) + { + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + } + else + { + while (*p && *p != '\n') ret[pos++] = *p++; + if (*p == '\n') ret[pos++] = *p++; + } + } + + ret[pos] = '\0'; + return ret; +} + + +/** + * 提取顶层键的字符串值 (双引号或单引号), 返回 malloc 的 caller-free 字符串。 + * 键不存在、值不是字符串、或键位于段内 (非顶层) 时返回 NULL。 + */ +static char * +uvh_get_top_level_value (const char *content, const char *key) +{ + const char *p = content; + bool in_table = false; + while (*p) + { + if (*p == '[') + { + in_table = true; + } + else if (!in_table && uvh_key_prefix (p, key)) + { + const char *v = p + strlen (key); + while (*v == ' ' || *v == '\t') v++; + if (*v != '=') return NULL; + v++; + while (*v == ' ' || *v == '\t') v++; + if (*v != '"' && *v != '\'') return NULL; + char quote = *v; + v++; + const char *val = v; + while (*v && *v != quote && *v != '\n') v++; + size_t vl = v - val; + char *ret = malloc (vl + 1); + memcpy (ret, val, vl); + ret[vl] = '\0'; + return ret; + } + p = uvh_next_line (p); + } + return NULL; +} diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c index 16c02bb5..7a08d8c5 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv.c @@ -4,6 +4,12 @@ def_target(pl_python_uv, "uv"); +#include "uv-helper.c" + +/* 内部 target 前置声明 (定义在文件末尾 def_target 处) */ +void pl_uv_github_release_prelude (void); +extern Target_t pl_uv_github_release_target; + void pl_python_uv_prelude (void) { @@ -13,7 +19,7 @@ pl_python_uv_prelude (void) chef_set_recipe_last_updated (this, "2026-08-02"); chef_set_chefs (this, 2, "@happy-game", "@MingriLingran"); - chef_set_sauciers (this, 2, "@Kattos", "@ccmywish"); + chef_set_sauciers (this, 3, "@Kattos", "@ccmywish", "@Mikachu2333"); chef_set_scope_cap (this, ProjectScope, ScopeCap_Able_And_Implemented); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); @@ -24,17 +30,20 @@ pl_python_uv_prelude (void) chef_allow_user_define(this); chef_use_other_target_sources (this, &pl_python_group_target); + + /* 内部 target 的 prelude 未通过 menu.c 的 add() 注册, 手动挂载 */ + pl_uv_github_release_target.preludefn = pl_uv_github_release_prelude; } /** * chsrc get uv * - * uv的配置优先级顺序如下(高到低): - * 1. $workspaces/uv.toml - * 2. $workspaces/pyproject.toml - * 3. ~/.config/uv/uv.toml - * 4. /etc/uv/uv.toml + * uv 配置文件查找优先级: + * 1 ./uv.toml (项目级,已实现) + * 2. $workspaces/pyproject.toml (项目级,未实现,因过于罕见不考虑适配) + * 3. ~/.config/uv/uv.toml (用户级,已实现) + * 4. /etc/uv/uv.toml (系统级,考虑到权限问题不予考虑) */ #define PL_Python_uv_ConfigFile "uv.toml" @@ -67,7 +76,8 @@ pl_python_find_uv_config (bool mkdir) { chsrc_ensure_dir (config_dir); } - return xy_2strcat (config_dir, PL_Python_uv_ConfigFile); + char *result = xy_2strcat (config_dir, PL_Python_uv_ConfigFile); + return result; } else { @@ -81,14 +91,18 @@ pl_python_find_uv_config (bool mkdir) } } + void pl_python_uv_getsrc (char *option) { char *uv_config = pl_python_find_uv_config (false); - if (!chsrc_check_file (uv_config)) + if (!uv_config || !chsrc_check_file (uv_config)) { - chsrc_error2 ("未找到 uv 配置文件"); + if (!uv_config) + chsrc_error2 ("无法获取 uv 配置文件路径"); + else + chsrc_error2 ("未找到 uv 配置文件"); return; } @@ -105,82 +119,154 @@ pl_python_uv_getsrc (char *option) char *cmd = xy_str_gsub (RAWSTR_pl_python_get_uv_config, "@f@", uv_config); chsrc_run (cmd, RunOpt_Default); } + + /* 检查 Python 下载镜像 (顶层 python-install-mirror 键) */ + char *content = xy_file_read (uv_config); + if (content) + { + char *mirror = uvh_get_top_level_value (content, "python-install-mirror"); + if (mirror) + { + say (mirror); + free (mirror); /* uvh_get_top_level_value 返回 caller-free */ + } + free (content); + } +} + + +/* + * Python下载镜像 (python-install-mirror) + * + * 内部 target, 不注册到 menu, 仅用于 chsrc_yield_source 自动测速选取。 + * 针对 python-build-standalone 的测速链接。 +*/ + +def_target (pl_uv_github_release, NULL); + +/* 内部 target, 无 CLI 入口, 以下为占位 */ +void pl_uv_github_release_getsrc (char *o) { (void)o; } +void pl_uv_github_release_setsrc (char *o) { (void)o; } +void pl_uv_github_release_resetsrc (char *o) { (void)o; } + +void +pl_uv_github_release_prelude (void) +{ + chef_prep_this (pl_uv_github_release, gsr); + + chef_set_recipe_created_on (this, "2026-05-31"); + chef_set_recipe_last_updated (this, "2026-05-31"); + + chef_set_chefs (this, 1, "@Mikachu2333"); + chef_set_sauciers (this, 0); + + chef_allow_english (this); + chef_allow_user_define (this); + + def_sources_begin () + {&UpstreamProvider, "https://github.com/astral-sh/python-build-standalone/releases/download", DelegateToUpstream}, + {&Nju, "https://mirrors.nju.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude}, + {&Ustc, "https://mirrors.ustc.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude}, + {&Lzuoss, "https://mirror.lzu.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude} + def_sources_end () + +#define GH_SM_POSTFIX "/20260510/cpython-3.14.5+20260510-i686-pc-windows-msvc-install_only_stripped.tar.gz" + chef_set_smURL_with_postfix (this, &Nju, GH_SM_POSTFIX); + chef_set_smURL_with_postfix (this, &Lzuoss, GH_SM_POSTFIX); +#undef GH_SM_POSTFIX + + /* 2026-5-31: USTC 仅保留 Latest, 只能用 SHA256SUMS 粗略测速 */ + chef_set_smURL_with_postfix (this, &Ustc, "/LatestRelease/SHA256SUMS"); + + /* 中科大仅保留 Latest 且文件内含动态版本号, 使用模糊测速 */ + chef_set_provider_sm_accuracy (&Ustc, ROUGH); +} + + +/** + * 一次性完成uv配置文件的全部文件操作 (set 路径) + */ +static void +pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char *py_dl_url) +{ + char *content = xy_file_read (uv_config); + if (!content) content = xy_strdup (""); + + char *updated = replace_pypi_index_url (content, pypi_url); + + char *final = replace_python_install_mirror (updated, py_dl_url); + + chsrc_overwrite_file (final, uv_config); } /** - * @consult https://docs.astral.sh/uv/configuration/files/ - * https://github.com/RubyMetric/chsrc/issues/139 + * chsrc set uv + * + * 同时更换两部分: + * 1. PyPI 索引源 ([[index]] 表) + * 2. Python 解释器下载源 (python-install-mirror) + * + * @consult https://docs.astral.sh/uv/reference/settings/#python-install-mirror */ void pl_python_uv_setsrc (char *option) { chsrc_ensure_program ("uv"); - Source_t source = chsrc_yield_source (&pl_python_group_target, option); - if (chsrc_in_standalone_mode()) - chsrc_confirm_source(&source); - char *uv_config = pl_python_find_uv_config (true); - if (NULL==uv_config) + if (NULL == uv_config) { chsrc_error2 ("无法获取 uv 配置文件路径"); return; } - chsrc_backup (uv_config); - const char *source_content = xy_str_gsub (RAWSTR_pl_python_uv_config_source_content, "@url@", source.url); - - if (!xy_file_exist (uv_config)) + /* reset: 清理 [[index]] 段 url 与 python-install-mirror 行 */ + if (chsrc_in_reset_mode ()) { - /* 当 uv_config 不存在,直接写入文件 */ - chsrc_append_to_file (source_content, uv_config); - } - else - { - /* 当 uv_config 存在,如果存在 [[index]] 则更新,否则追加到文件末尾 */ - char *cmd = NULL; - if (xy.on_windows) - { - /* 在 Windows 上使用 PowerShell 替代 grep */ - cmd = xy_str_gsub (RAWSTR_pl_python_test_uv_if_set_source_on_windows, "@f@", uv_config); - } - else + if (!chsrc_check_file (uv_config)) { - cmd = xy_str_gsub (RAWSTR_pl_python_test_uv_if_set_source, "@f@", uv_config); + chsrc_info ("没有 uv 配置文件, 无需重置"); + return; } - int status = xy_run_get_status (cmd); - if (0==status) - { - if (xy.on_windows) - { - /* 在 Windows 上使用 PowerShell 替代 sed */ - char *powershell_cmd_with_file = xy_str_gsub(RAWSTR_pl_python_set_uv_config_on_windows, "@f@", uv_config); - char *powershell_cmd = xy_str_gsub(powershell_cmd_with_file, "@url@", source.url); - chsrc_run (powershell_cmd, RunOpt_Default); - } - else - { - /* 非 Windows 系统使用 sed */ - char *sed_cmd = NULL; -#if defined(XY_Build_On_macOS) || defined(XY_Build_On_BSD) - sed_cmd = "sed -i '' "; -#else - sed_cmd = "sed -i "; -#endif - char *update_config_cmd = xy_str_gsub (RAWSTR_pl_python_set_uv_config, "@sed@", sed_cmd); - update_config_cmd = xy_str_gsub (update_config_cmd, "@f@", uv_config); - update_config_cmd = xy_str_gsub (update_config_cmd, "@url@", source.url); - chsrc_run (update_config_cmd, RunOpt_Default); - } - } - else + /* 读内容, 清理, 写回 */ + char *content = xy_file_read (uv_config); + if (!content) { - chsrc_append_to_file (source_content, uv_config); + chsrc_error2 ("无法读取 uv 配置文件"); + return; } + + char *cleaned = cleanup_config_for_reset (content); + chsrc_overwrite_file (cleaned, uv_config); + + return; } + /* set: 选取源并写入 */ + Source_t source = chsrc_yield_source (&pl_python_group_target, option); + + /* 若 option 命中了 GitHub release 源则直接用, 否则自动测速 */ + char *gh_opt = option; + if (gh_opt && !chsrc_in_reset_mode () && !hp_is_url (gh_opt)) + { + bool found = false; + if (!pl_uv_github_release_target.inited) + pl_uv_github_release_target.preludefn (); + for (int i = 0; i < pl_uv_github_release_target.sources_n; i++) + if (xy_streql (pl_uv_github_release_target.sources[i].mirror->code, gh_opt)) + { found = true; break; } + if (!found) gh_opt = NULL; + } + Source_t gh_source = chsrc_yield_source (&pl_uv_github_release_target, gh_opt); + + if (chsrc_in_standalone_mode()) + chsrc_confirm_source (&source); + + chsrc_backup (uv_config); + pl_python_uv_write_all (uv_config, source.url, gh_source.url); + if (chsrc_in_standalone_mode()) { chsrc_determine_chgtype (ChgType_Auto); diff --git a/test/uv-helper-test.c b/test/uv-helper-test.c new file mode 100644 index 00000000..7ff34c37 --- /dev/null +++ b/test/uv-helper-test.c @@ -0,0 +1,264 @@ +/** ------------------------------------------------------------ + * SPDX-License-Identifier: GPL-3.0-or-later + * ------------------------------------------------------------- + * uv-helper.c (uv.toml 字符串改写工具) 单元测试 + * + * 独立编译: 不依赖 xy.h / framework, 直接 #include uv-helper.c + * ------------------------------------------------------------*/ + +#include +#include +#include + +#include "../src/recipe/lang/Python/uv-helper.c" + +static int total = 0; +static int failures = 0; + +#define CHECK(cond) \ + do { \ + total++; \ + if (!(cond)) \ + { \ + failures++; \ + printf ("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + +#define CHECK_STR(got, expect) \ + do { \ + total++; \ + if (strcmp ((got), (expect)) != 0) \ + { \ + failures++; \ + printf ("FAIL %s:%d:\n expected [%s]\n got [%s]\n", \ + __FILE__, __LINE__, (expect), (got)); \ + } \ + } while (0) + + +static void +test_replace_pypi_index_url (void) +{ + char *r; + + /* 空文件: 追加整个段 */ + r = replace_pypi_index_url ("", "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* 已有 [[index]] 与 url: 仅替换 url 行 */ + r = replace_pypi_index_url ( + "[[index]]\nurl = \"https://old\"\ndefault = true\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* default = false 保留 */ + r = replace_pypi_index_url ( + "[[index]]\nurl = \"https://old\"\ndefault = false\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = false\n"); + free (r); + + /* [[index]] 段内没有 url 键: 在段内第一行之前插入 url + default */ + r = replace_pypi_index_url ( + "[[index]]\nname = \"tuna\"\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\nname = \"tuna\"\n"); + free (r); + + /* 注释中的 [[index]] 不应被当作真实段: 追加新段 */ + r = replace_pypi_index_url ( + "# [[index]]\nfoo = 1\n", + "https://mirror"); + CHECK_STR (r, "# [[index]]\nfoo = 1\n[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* 注释与真实段并存: 修改真实段 */ + r = replace_pypi_index_url ( + "# [[index]]\n[[index]]\nurl = \"https://old\"\n", + "https://mirror"); + CHECK_STR (r, "# [[index]]\n[[index]]\nurl = \"https://mirror\"\n"); + free (r); + + /* 单引号字符串值也能被替换 (替换后统一为双引号) */ + r = replace_pypi_index_url ( + "[[index]]\nurl = 'https://old'\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n"); + free (r); + + /* 多个 [[index]] 段: 只修改第一个 */ + r = replace_pypi_index_url ( + "[[index]]\nurl = \"https://old1\"\n\n[[index]]\nurl = \"https://old2\"\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n\n[[index]]\nurl = \"https://old2\"\n"); + free (r); + + /* 段边界: [[index]] 之后其他表的 url 键不被误改 */ + r = replace_pypi_index_url ( + "[[index]]\nurl = \"https://old\"\n\n[python]\nurl = \"https://python\"\n", + "https://mirror"); + CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n\n[python]\nurl = \"https://python\"\n"); + free (r); +} + + +static void +test_replace_python_install_mirror (void) +{ + char *r; + + /* 空文件 */ + r = replace_python_install_mirror ("", "https://mirror"); + CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n"); + free (r); + + /* 顶层已有: 就地替换 */ + r = replace_python_install_mirror ( + "python-install-mirror = \"https://old\"\nother = 1\n", + "https://mirror"); + CHECK_STR (r, "python-install-mirror = \"https://mirror\"\nother = 1\n"); + free (r); + + /* 顶层键在段头之前: 替换后仍是顶层键 */ + r = replace_python_install_mirror ( + "python-install-mirror = \"https://old\"\n\n[python]\nx = 1\n", + "https://mirror"); + CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n\n[python]\nx = 1\n"); + free (r); + + /* 顶层无旧键但有段: 在第一个段头之前插入, 保证新键仍属顶层 */ + r = replace_python_install_mirror ( + "[python]\nx = 1\n", + "https://mirror"); + CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n[python]\nx = 1\n"); + free (r); + + /* 段内同名键不受影响 (chsrc 只管理顶层键) */ + r = replace_python_install_mirror ( + "[python]\npython-install-mirror = \"https://old\"\n", + "https://mirror"); + CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n[python]\npython-install-mirror = \"https://old\"\n"); + free (r); + + /* 注释行不误删 */ + r = replace_python_install_mirror ( + "# python-install-mirror = \"comment\"\npython-install-mirror = \"https://old\"\n", + "https://mirror"); + CHECK_STR (r, "# python-install-mirror = \"comment\"\npython-install-mirror = \"https://mirror\"\n"); + free (r); +} + + +static void +test_cleanup_config_for_reset (void) +{ + char *r; + + /* 空内容 */ + r = cleanup_config_for_reset (""); + CHECK_STR (r, ""); + free (r); + + /* 只清理 [[index]] 的 url/default 与顶层 python-install-mirror; + * 其他表 ([python]) 内的 url 键必须保留 */ + r = cleanup_config_for_reset ( + "python-install-mirror = \"https://old\"\n" + "[[index]]\n" + "name = \"tuna\"\n" + "url = \"https://old\"\n" + "default = true\n" + "\n" + "[python]\n" + "url = \"https://python\"\n"); + CHECK_STR (r, + "[[index]]\n" + "name = \"tuna\"\n" + "\n" + "[python]\n" + "url = \"https://python\"\n"); + free (r); + + /* 顶层其他键保留 */ + r = cleanup_config_for_reset ( + "a = 1\npython-install-mirror = \"https://old\"\nb = 2\n"); + CHECK_STR (r, "a = 1\nb = 2\n"); + free (r); + + /* 注释行保留 (注释中的 url 键不误删) */ + r = cleanup_config_for_reset ( + "# url = \"comment\"\n[[index]]\nurl = \"real\"\n"); + CHECK_STR (r, "# url = \"comment\"\n[[index]]\n"); + free (r); + + /* 多个 [[index]] 段都清理 */ + r = cleanup_config_for_reset ( + "[[index]]\nurl = \"https://old1\"\n\n[[index]]\nurl = \"https://old2\"\n"); + CHECK_STR (r, "[[index]]\n\n[[index]]\n"); + free (r); + + /* 其他表内的 default 键保留 */ + r = cleanup_config_for_reset ( + "[python]\ndefault = \"x\"\n"); + CHECK_STR (r, "[python]\ndefault = \"x\"\n"); + free (r); +} + + +static void +test_uvh_get_top_level_value (void) +{ + char *v; + + v = uvh_get_top_level_value ( + "python-install-mirror = \"https://x\"\n", "python-install-mirror"); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://x"); free (v); } + + /* 单引号 */ + v = uvh_get_top_level_value ( + "python-install-mirror = 'https://x'\n", "python-install-mirror"); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://x"); free (v); } + + /* 等号周围无空格 */ + v = uvh_get_top_level_value ( + "python-install-mirror=\"https://x\"\n", "python-install-mirror"); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://x"); free (v); } + + /* 键位于段内 (非顶层) 时返回 NULL */ + v = uvh_get_top_level_value ( + "[python]\npython-install-mirror = \"https://x\"\n", "python-install-mirror"); + CHECK (v == NULL); + + /* 键不存在时返回 NULL */ + v = uvh_get_top_level_value ("other = 1\n", "python-install-mirror"); + CHECK (v == NULL); + + /* 注释行中的键不匹配 */ + v = uvh_get_top_level_value ( + "# python-install-mirror = \"comment\"\n", "python-install-mirror"); + CHECK (v == NULL); +} + + +int +main (void) +{ + test_replace_pypi_index_url (); + test_replace_python_install_mirror (); + test_cleanup_config_for_reset (); + test_uvh_get_top_level_value (); + + if (failures) + { + printf ("uv-helper-test: %d/%d checks FAILED\n", failures, total); + return 1; + } + + printf ("uv-helper-test: all %d checks passed\n", total); + return 0; +} From abcf7891428ad48200b11b676e6b4cad3ec978b8 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 21:00:05 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9C=A8=20pyproject.t?= =?UTF-8?q?oml=20=E4=B8=AD=E9=85=8D=E7=BD=AE=20uv=20=E6=8D=A2=E6=BA=90?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=96=B0=E5=A2=9E=20CNB=20=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\350\257\264\346\230\216.md" | 96 ++++ src/recipe/lang/Python/uv-helper.c | 520 ++++++++++++------ src/recipe/lang/Python/uv.c | 153 ++++-- test/uv-helper-test.c | 204 +++++-- 4 files changed, 708 insertions(+), 265 deletions(-) create mode 100644 "doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" diff --git "a/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" "b/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" new file mode 100644 index 00000000..a310d2cb --- /dev/null +++ "b/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" @@ -0,0 +1,96 @@ + + +# uv 换源实现说明 + +`chsrc set uv` 同时配置两类下载地址: + +1. Python 包索引,即 `uv.toml` 的 `[[index]]` 或 `pyproject.toml` 的 `[[tool.uv.index]]` +2. uv 管理的 CPython 下载地址,即 `python-install-mirror` + +
+ + +## 配置文件选择 + +项目作用域只修改当前目录,不向父目录查找并修改配置: + +1. 当前目录存在 `uv.toml` 时修改 `uv.toml` +2. 否则,当前目录存在 `pyproject.toml` 时修改其中的 `[tool.uv]` +3. 两者都不存在时创建 `uv.toml` + +这个顺序与 uv 的行为一致:同一目录同时存在 `uv.toml` 与 +`pyproject.toml` 的 `[tool.uv]` 时,uv 优先使用 `uv.toml` 中的对应字段。 + +用户作用域使用 uv 的用户配置目录:Windows 为 `%APPDATA%\uv\uv.toml`, +Linux 和 macOS 为 `~/.config/uv/uv.toml`。 + +
+ + +## Python 包索引 + +索引 URL 必须指向兼容 Python Simple Repository API 的地址,通常以 +`/simple` 结尾;uv 不要求 URL 必须以 `/` 结尾。 + +`default = true` 表示该 index 替代 uv 内置的 PyPI,并且始终具有最低 +index 优先级。它不表示“文本中的第一个”或“最高优先级”。配置中存在多个 +index 时,chsrc 优先更新 `default = true` 的 index;没有 default index +时才更新第一个 index。 + +默认的 `first-index` 策略会在第一个包含目标包的 index 停止搜索。私有源 +与公共镜像并存时,不应把它理解成跨所有 index 选择全局最新版本。 + +
+ + +## Python 下载镜像 + +`python-install-mirror` 是 +`https://github.com/astral-sh/python-build-standalone/releases/download` +的替换前缀。配置值必须是镜像侧对应的 `releases/download` 基址,不能是 +仓库主页、某个具体构建日期或某个归档文件。uv 会在其后追加构建日期和 +归档文件名。 + +官方示例使用不带尾部 `/` 的基址,chsrc 内置源也遵循该形式。 + +
+ + +## reset 与备份 + +`chsrc set uv` 修改已有配置前会创建 `.bak` 备份。 + +`chsrc reset uv` 把 chsrc 管理的 index 与 `python-install-mirror` 写回 +默认上游:index 恢复为 `https://pypi.org/simple`, +`python-install-mirror` 恢复为 +`https://github.com/astral-sh/python-build-standalone/releases/download`。 +其他用户配置原样保留,不依赖 `.bak` 恢复旧 URL。 + +`default = true` 的 index(不存在时取第一个)被视为 PyPI 替代源, +reset 只修改这一处;私有 index 不受影响。 + +
+ + +## 实现边界 + +配置改写器只解析 chsrc 使用的 TOML 子集,但支持常见的行首缩进、CRLF、 +行尾注释、多个 index,以及位于 `[tool.uv.sources]` 等子表之后的 +`[[tool.uv.index]]`。改写会保留原有缩进与行尾风格(CRLF/LF)。多行字符串 +和完整 TOML 转义语义不属于该轻量改写器的支持范围。 + +参考: + +- [uv index 设置](https://docs.astral.sh/uv/reference/settings/#index) +- [uv python-install-mirror 设置](https://docs.astral.sh/uv/reference/settings/#python-install-mirror) +- [uv 配置文件](https://docs.astral.sh/uv/concepts/configuration-files/) +- [uv package indexes](https://docs.astral.sh/uv/concepts/indexes/) + +
diff --git a/src/recipe/lang/Python/uv-helper.c b/src/recipe/lang/Python/uv-helper.c index 592894cd..4a6b84de 100644 --- a/src/recipe/lang/Python/uv-helper.c +++ b/src/recipe/lang/Python/uv-helper.c @@ -8,14 +8,16 @@ * Last Modified : <2026-08-02> * * - * uv.toml 配置字符串改写工具 (由 uv.c #include, 可被 test 单独 #include) + * uv 配置 (uv.toml / pyproject.toml) 字符串改写工具 + * (由 uv.c #include, 可被 test 单独 #include) * * 仅处理 chsrc 关心的 uv 配置子集, 不是完整 TOML 解析器: - * - [[index]] 数组表: 替换/清理其 url、default 行 (保留段头与其他键) - * - 顶层 python-install-mirror 键: 替换/删除 + * - uv.toml: 顶层 [[index]] 数组表、顶层 python-install-mirror 键 + * - pyproject.toml: [tool.uv] 表内的 [[tool.uv.index]] 数组表、 + * python-install-mirror 键 * * 刻意不处理: 多行字符串、数组/内联表值、字符串转义 —— - * 因为所有改写都按整行“替换/删除”进行, 无需解析值本身。 + * 因为所有改写都按"整行替换/删除"进行, 无需解析值本身。 * 已知限制: 多行字符串 (""") 内若出现行首 '[' 会被误认为段头, 罕见可忽略。 * * 所有函数均为 static, 依赖调用方所在翻译单元提供 stdlib/string.h。 @@ -39,15 +41,25 @@ uvh_key_prefix (const char *p, const char *key) return c == '=' || c == ' ' || c == '\t' || c == '\n' || c == '\0'; } +static const char * +uvh_skip_indent (const char *p) +{ + while (*p == ' ' || *p == '\t') p++; + return p; +} + /** - * 判断行首是否为 [[index]] 数组表头 (允许行尾注释, 如 "[[index]] # note")。 + * 判断行首 (调用方必须保证 p 位于行首) 是否为指定表头文本。 + * 允许缩进、CRLF 和行尾注释。 */ static bool -uvh_is_index_header (const char *p) +uvh_header_match (const char *p, const char *header) { - if (strncmp (p, "[[index]]", 9) != 0) return false; - char c = p[9]; - return c == '\n' || c == '\0' || c == ' ' || c == '\t'; + p = uvh_skip_indent (p); + size_t hl = strlen (header); + if (strncmp (p, header, hl) != 0) return false; + char c = p[hl]; + return c == '\n' || c == '\r' || c == '\0' || c == ' ' || c == '\t' || c == '#'; } /* 跳过当前行 (含换行符), 返回下一行的行首 */ @@ -60,7 +72,7 @@ uvh_next_line (const char *p) } /** - * 从 start (段内第一行) 开始找下一个段头行 (行首 '[') 的位置。 + * 从 start 开始找下一个段头行 (行首 '[') 的位置。 * 返回 NULL 表示直到文件结尾都没有新段头。 */ static const char * @@ -69,237 +81,427 @@ uvh_find_section_end (const char *start) const char *p = start; while (*p) { - if (*p == '[') return p; + if (*uvh_skip_indent (p) == '[') return p; p = uvh_next_line (p); } return NULL; } - /** - * 在 content 中找到第一个 [[index]] 段, 替换其 url = "..." 行 (或单引号值)。 - * 文件中没有 [[index]] 段时, 追加整个段到末尾。 - * - * 只修改第一个 [[index]] 段; 段内 url/default 键的语义由调用者 (set) 决定。 - * - * @return 新内容 (caller-free) + * 全文件查找第一个行首匹配 header 的表头位置; 未找到返回 NULL。 */ -static char * -replace_pypi_index_url (const char *content, const char *url) +static const char * +uvh_find_table (const char *content, const char *header) { - /* 找第一个行首 [[index]] 段头 (跳过注释等非段头匹配) */ const char *p = content; - const char *index_header = NULL; while (*p) { - if (*p == '[' && uvh_is_index_header (p)) { index_header = p; break; } + const char *line = uvh_skip_indent (p); + if (*line == '[' && uvh_header_match (line, header)) return p; p = uvh_next_line (p); } + return NULL; +} - /* 文件中尚无 [[index]] 段, 追加到末尾 */ - if (!index_header) +/** + * 在 [first, end) 行区间内查找以 key 开头的键行; end 为 NULL 时到文件尾。 + */ +static const char * +uvh_find_key_in_section (const char *first, const char *end, const char *key) +{ + for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) { - size_t len = strlen (content) + strlen (url) + 96; - char *ret = calloc (len, 1); - size_t clen = strlen (content); - if (clen > 0 && content[clen - 1] == '\n') - snprintf (ret, len, "%s[[index]]\nurl = \"%s\"\ndefault = true\n", content, url); - else if (clen > 0) - snprintf (ret, len, "%s\n[[index]]\nurl = \"%s\"\ndefault = true\n", content, url); - else - snprintf (ret, len, "[[index]]\nurl = \"%s\"\ndefault = true\n", url); - return ret; + if (uvh_key_prefix (uvh_skip_indent (s), key)) return s; } + return NULL; +} - /* 段内第一行与段边界 (下一个段头或文件尾) */ - const char *first = uvh_next_line (index_header); - const char *end = uvh_find_section_end (first); +/** + * 从 "key = \"value\"" 或 "key = 'value'" 行提取字符串值。 + * @return malloc 的 caller-free 字符串; 值不是字符串时返回 NULL。 + */ +static char * +uvh_extract_string_value (const char *line) +{ + const char *eq = strchr (uvh_skip_indent (line), '='); + if (!eq) return NULL; + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; + if (*v != '"' && *v != '\'') return NULL; + char quote = *v; + v++; + const char *val = v; + while (*v && *v != quote && *v != '\n') v++; + size_t vl = v - val; + char *ret = malloc (vl + 1); + memcpy (ret, val, vl); + ret[vl] = '\0'; + return ret; +} - /* 在当前 [[index]] 段内找 url 键行 */ - const char *url_line = NULL; - for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) +static bool +uvh_value_is_true (const char *line) +{ + const char *eq = strchr (uvh_skip_indent (line), '='); + if (!eq) return false; + const char *v = uvh_skip_indent (eq + 1); + return strncmp (v, "true", 4) == 0 + && (v[4] == '\0' || v[4] == '\r' || v[4] == '\n' || v[4] == ' ' || v[4] == '\t' || v[4] == '#'); +} + +/** + * 查找要管理的 index。多个 index 中优先选择 default = true, + * 否则选择第一个;表可以位于 [tool.uv.sources] 等子表之后。 + */ +static const char * +uvh_find_managed_index (const char *content, const char *index_header) +{ + const char *first = NULL; + for (const char *s = content; *s; s = uvh_next_line (s)) { - if (uvh_key_prefix (s, "url")) { url_line = s; break; } + if (!uvh_header_match (s, index_header)) continue; + if (!first) first = s; + + const char *body = uvh_next_line (s); + const char *end = uvh_find_section_end (body); + const char *default_line = uvh_find_key_in_section (body, end, "default"); + if (default_line && uvh_value_is_true (default_line)) return s; } + return first; +} - /* 段内没有 url 键: 在段内第一行之前插入 url 行 (及缺失的 default 行) */ - if (!url_line) - { - bool has_default = false; - for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) - { - if (uvh_key_prefix (s, "default")) { has_default = true; break; } - } +/* 文件是否使用 CRLF 换行。 */ +static bool +uvh_detect_crlf (const char *content) +{ + const char *nl = strchr (content, '\n'); + return nl && nl > content && nl[-1] == '\r'; +} - size_t len = strlen (content) + strlen (url) + 64; - char *ret = calloc (len, 1); - size_t pos = 0; - pos += snprintf (ret + pos, len - pos, "%.*s", (int)(first - content), content); - pos += snprintf (ret + pos, len - pos, "url = \"%s\"\n", url); - if (!has_default) - pos += snprintf (ret + pos, len - pos, "default = true\n"); - strcpy (ret + pos, first); - return ret; +/** + * 按 content 的换行风格转换 str 中的 LF;content 为 CRLF 时返回 CRLF 版本。 + * 仅用于追加的纯 LF 文本,不要传入已含 CRLF 的 content 本身。 + * + * @return caller-free + */ +static char * +uvh_apply_eol (const char *content, const char *str) +{ + bool crlf = uvh_detect_crlf (content); + size_t n = strlen (str); + char *ret = malloc (n + (crlf ? n : 0) + 1); + size_t pos = 0; + for (const char *s = str; *s; s++) + { + if (crlf && *s == '\n') ret[pos++] = '\r'; + ret[pos++] = *s; } + ret[pos] = '\0'; + return ret; +} - /* 替换 url 行, 保留该行原有的换行符与后续内容 */ - const char *line_end = strchr (url_line, '\n'); +/** + * 用 new_line_text (不含换行) 替换 content 中从 line 开始的整行, + * 保留原行缩进、行尾风格 (CRLF/LF) 与后续内容。 + * + * @return 新内容 (caller-free) + */ +static char * +uvh_replace_line (const char *content, const char *line, const char *new_line_text) +{ + const char *line_end = strchr (line, '\n'); if (!line_end) line_end = content + strlen (content); - size_t len = strlen (content) + strlen (url) + 32; + /* 原行为 CRLF 时,新行同样使用 CRLF */ + bool crlf = (*line_end == '\n' && line_end > line && line_end[-1] == '\r'); + + /* 复用原行缩进 */ + const char *indent_end = uvh_skip_indent (line); + size_t ind = indent_end - line; + + size_t len = strlen (content) + ind + strlen (new_line_text) + 8; char *ret = calloc (len, 1); size_t pos = 0; - pos += snprintf (ret + pos, len - pos, "%.*s", (int)(url_line - content), content); - pos += snprintf (ret + pos, len - pos, "url = \"%s\"", url); + pos += snprintf (ret + pos, len - pos, "%.*s", (int)(line - content), content); + pos += snprintf (ret + pos, len - pos, "%.*s", (int)ind, line); + pos += snprintf (ret + pos, len - pos, "%s", new_line_text); + if (crlf) ret[pos++] = '\r'; strcpy (ret + pos, line_end); return ret; } +/** + * 在 insert_at 位置之前插入 insert_text (以 '\n' 结尾)。 + * 内容为 CRLF 时,插入文本与分隔换行同样转换为 CRLF。 + * + * @return 新内容 (caller-free) + */ +static char * +uvh_insert_before (const char *content, const char *insert_at, const char *insert_text) +{ + size_t head = insert_at - content; + + bool need_sep = (head > 0 && content[head - 1] != '\n'); + bool crlf = need_sep ? uvh_detect_crlf (content) + : (head >= 2 && content[head - 1] == '\n' && content[head - 2] == '\r'); + + size_t text_len = strlen (insert_text); + size_t extra = (crlf ? text_len : 0) + (need_sep ? 1 : 0); + size_t len = strlen (content) + text_len + extra + 16; + char *ret = calloc (len, 1); + size_t pos = 0; + pos += snprintf (ret + pos, len - pos, "%.*s", (int)head, content); + if (need_sep) + { + if (crlf) ret[pos++] = '\r'; + ret[pos++] = '\n'; + } + for (const char *t = insert_text; *t; t++) + { + if (crlf && *t == '\n') ret[pos++] = '\r'; + ret[pos++] = *t; + } + strcpy (ret + pos, content + head); + return ret; +} + /** - * 替换顶层 python-install-mirror 键的值; 顶层无该键时, 追加到顶层区域末尾 - * (第一个段头之前), 保证新键仍是顶层键而非被归入某个表。 + * 替换/创建 index 数组表 (如 [[index]] 或 [[tool.uv.index]]) 的 url 值。 * - * 段内 (非顶层) 的同名键不受影响 —— chsrc 只管理顶层键。 + * @param content 原始内容 + * @param url 新的索引 URL + * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; + * 父表不存在时会在文件末尾创建父表并写入 index 段 * * @return 新内容 (caller-free) */ static char * -replace_python_install_mirror (const char *content, const char *url) +replace_index_url (const char *content, const char *url, const char *index_header, const char *parent_table) { - /* 只扫描顶层区域 (第一个段头之前) */ - const char *p = content; - const char *old_line = NULL; - const char *first_section = NULL; - while (*p) + /* default index 才是 uv 的 PyPI 替代源;不存在 default 时回退到第一个。 */ + const char *ih = uvh_find_managed_index (content, index_header); + const char *boundary = content + strlen (content); + if (!ih && parent_table) { - if (*p == '[') + const char *table = uvh_find_table (content, parent_table); + if (!table) { - first_section = p; - break; + /* 父表不存在: 在文件末尾创建父表并在其中创建 index 段, + * 换行风格与 content 保持一致 */ + size_t clen = strlen (content); + const char *sep = (clen > 0) ? "\n" : ""; + size_t seg_len = strlen (parent_table) + strlen (index_header) + strlen (url) + 48; + char *seg = calloc (seg_len, 1); + snprintf (seg, seg_len, "%s\n%s\nurl = \"%s\"\ndefault = true\n", + parent_table, index_header, url); + char *seg_eol = uvh_apply_eol (content, seg); + free (seg); + char *sep_eol = uvh_apply_eol (content, sep); + size_t len = clen + strlen (sep_eol) + strlen (seg_eol) + 1; + char *ret = calloc (len, 1); + snprintf (ret, len, "%s%s%s", content, sep_eol, seg_eol); + free (sep_eol); + free (seg_eol); + return ret; } - if (uvh_key_prefix (p, "python-install-mirror")) old_line = p; - p = uvh_next_line (p); } - size_t base = strlen (content) + strlen (url) + 64; - - /* 顶层已有旧行: 就地替换 */ - if (old_line) + /* 作用域内无 index 段: 追加到边界之前 */ + if (!ih) { - const char *line_end = strchr (old_line, '\n'); - if (!line_end) line_end = content + strlen (content); - - char *ret = calloc (base, 1); - size_t pos = 0; - pos += snprintf (ret + pos, base - pos, "%.*s", (int)(old_line - content), content); - pos += snprintf (ret + pos, base - pos, "python-install-mirror = \"%s\"", url); - strcpy (ret + pos, line_end); + size_t seglen = strlen (index_header) + strlen (url) + 48; + char *seg = malloc (seglen); + snprintf (seg, seglen, "%s\nurl = \"%s\"\ndefault = true\n", index_header, url); + char *ret = uvh_insert_before (content, boundary, seg); + free (seg); return ret; } - /* 顶层无旧行: 追加到顶层区域末尾 (无段头则文件末尾) */ - const char *insert_at = first_section ? first_section : content + strlen (content); - size_t head = insert_at - content; + /* 段内第一行与段边界 (下一个段头或文件尾) */ + const char *first = uvh_next_line (ih); + const char *end = uvh_find_section_end (first); + const char *url_line = uvh_find_key_in_section (first, end, "url"); - char *ret = calloc (base, 1); - size_t pos = 0; - pos += snprintf (ret + pos, base - pos, "%.*s", (int)head, content); - if (head > 0 && content[head - 1] != '\n') ret[pos++] = '\n'; - pos += snprintf (ret + pos, base - pos, "python-install-mirror = \"%s\"\n", url); - strcpy (ret + pos, content + head); + /* 段内没有 url 键: 在段内第一行之前插入 url (及缺失的 default) */ + if (!url_line) + { + bool has_default = uvh_find_key_in_section (first, end, "default") != NULL; + size_t seglen = strlen (url) + (has_default ? 24 : 48); + char *seg = malloc (seglen); + size_t spos = 0; + spos += snprintf (seg + spos, seglen - spos, "url = \"%s\"\n", url); + if (!has_default) + spos += snprintf (seg + spos, seglen - spos, "default = true\n"); + char *ret = uvh_insert_before (content, first, seg); + free (seg); + return ret; + } + + /* 替换 url 行 */ + size_t newlen = strlen (url) + 32; + char *new_line = malloc (newlen); + snprintf (new_line, newlen, "url = \"%s\"", url); + char *ret = uvh_replace_line (content, url_line, new_line); + free (new_line); return ret; } /** - * reset 专用: 删除每个 [[index]] 段内的 url/default 行 (保留段头与 - * name 等其余字段), 删除顶层 python-install-mirror 行, 其余内容原样保留。 + * 替换/创建顶层键或指定表内键 (如 python-install-mirror) 的字符串值。 * - * 段边界严格限定: 其他表 (如 [python]) 内同名的 url/default 键不受影响。 + * @param content 原始内容 + * @param key 键名 + * @param url 新值 + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; + * 父表不存在时会在文件末尾创建父表并写入键 * * @return 新内容 (caller-free) */ static char * -cleanup_config_for_reset (const char *content) +replace_key_value (const char *content, const char *key, const char *url, const char *parent_table) { - size_t est = strlen (content) + 128; - char *ret = calloc (est, 1); - size_t pos = 0; - - bool in_table = false; /* 是否已进入任何表 (用于限定 python-install-mirror 仅顶层) */ - bool in_index = false; /* 当前是否处于 [[index]] 段 (用于限定 url/default) */ + const char *old_line = NULL; + const char *insert_at = NULL; - const char *p = content; - while (*p) + if (parent_table) { - bool skip = false; - - if (*p == '[') + const char *table = uvh_find_table (content, parent_table); + if (!table) { - in_table = true; - in_index = uvh_is_index_header (p); - } - else if (in_index && (uvh_key_prefix (p, "url") || uvh_key_prefix (p, "default"))) - { - skip = true; - } - else if (!in_table && uvh_key_prefix (p, "python-install-mirror")) - { - skip = true; + /* 父表不存在: 在文件末尾创建父表并写入键, + * 换行风格与 content 保持一致 */ + size_t clen = strlen (content); + const char *sep = (clen > 0) ? "\n" : ""; + size_t seg_len = strlen (parent_table) + strlen (key) + strlen (url) + 16; + char *seg = calloc (seg_len, 1); + snprintf (seg, seg_len, "%s\n%s = \"%s\"\n", parent_table, key, url); + char *seg_eol = uvh_apply_eol (content, seg); + free (seg); + char *sep_eol = uvh_apply_eol (content, sep); + size_t len = clen + strlen (sep_eol) + strlen (seg_eol) + 1; + char *ret = calloc (len, 1); + snprintf (ret, len, "%s%s%s", content, sep_eol, seg_eol); + free (sep_eol); + free (seg_eol); + return ret; } - if (skip) + const char *first = uvh_next_line (table); + const char *key_zone_end = NULL; + for (const char *s = first; *s; s = uvh_next_line (s)) { - while (*p && *p != '\n') p++; - if (*p == '\n') p++; + const char *line = uvh_skip_indent (s); + if (*line == '[') + { + key_zone_end = s; + break; + } } - else + old_line = uvh_find_key_in_section (first, key_zone_end, key); + insert_at = key_zone_end ? key_zone_end : content + strlen (content); + } + else + { + /* 顶层键区: 到第一个段头为止 */ + const char *p = content; + while (*p) { - while (*p && *p != '\n') ret[pos++] = *p++; - if (*p == '\n') ret[pos++] = *p++; + if (*uvh_skip_indent (p) == '[') { insert_at = p; break; } + if (uvh_key_prefix (uvh_skip_indent (p), key)) old_line = p; + p = uvh_next_line (p); } + if (!insert_at) insert_at = content + strlen (content); } - ret[pos] = '\0'; + if (old_line) + { + /* 替换旧值行 */ + size_t newlen = strlen (key) + strlen (url) + 32; + char *new_line = malloc (newlen); + snprintf (new_line, newlen, "%s = \"%s\"", key, url); + char *ret = uvh_replace_line (content, old_line, new_line); + free (new_line); + return ret; + } + + /* 键不存在: 追加到作用域末尾 */ + size_t seglen = strlen (key) + strlen (url) + 32; + char *seg = malloc (seglen); + snprintf (seg, seglen, "%s = \"%s\"\n", key, url); + char *ret = uvh_insert_before (content, insert_at, seg); + free (seg); return ret; } /** - * 提取顶层键的字符串值 (双引号或单引号), 返回 malloc 的 caller-free 字符串。 - * 键不存在、值不是字符串、或键位于段内 (非顶层) 时返回 NULL。 + * 在指定表 (parent_table 为 NULL 表示顶层) 内提取键的字符串值。 + * + * @return malloc 的 caller-free 字符串; 未找到返回 NULL。 */ static char * -uvh_get_top_level_value (const char *content, const char *key) +uvh_get_value_in_table (const char *content, const char *key, const char *parent_table) { const char *p = content; - bool in_table = false; - while (*p) + if (parent_table) { - if (*p == '[') - { - in_table = true; - } - else if (!in_table && uvh_key_prefix (p, key)) - { - const char *v = p + strlen (key); - while (*v == ' ' || *v == '\t') v++; - if (*v != '=') return NULL; - v++; - while (*v == ' ' || *v == '\t') v++; - if (*v != '"' && *v != '\'') return NULL; - char quote = *v; - v++; - const char *val = v; - while (*v && *v != quote && *v != '\n') v++; - size_t vl = v - val; - char *ret = malloc (vl + 1); - memcpy (ret, val, vl); - ret[vl] = '\0'; - return ret; - } - p = uvh_next_line (p); + const char *table = uvh_find_table (content, parent_table); + if (!table) return NULL; + p = uvh_next_line (table); + } + + const char *limit = uvh_find_section_end (p); + for (const char *s = p; *s && (!limit || s < limit); s = uvh_next_line (s)) + { + if (uvh_key_prefix (uvh_skip_indent (s), key)) return uvh_extract_string_value (s); } return NULL; } + + +/** + * 提取 chsrc 管理的 index URL:优先 default = true,否则取第一个。 + * + * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]" + * + * @return malloc 的 caller-free 字符串; 未找到返回 NULL。 + */ +static char * +uvh_get_index_url (const char *content, const char *index_header, const char *parent_table) +{ + if (parent_table && !uvh_find_table (content, parent_table)) return NULL; + + const char *ih = uvh_find_managed_index (content, index_header); + if (!ih) return NULL; + + const char *first = uvh_next_line (ih); + const char *end = uvh_find_section_end (first); + const char *url_line = uvh_find_key_in_section (first, end, "url"); + if (!url_line) return NULL; + return uvh_extract_string_value (url_line); +} + + +/* ============ uv.toml 便捷包装 (保持原函数名) ============ */ + +static char * +replace_pypi_index_url (const char *content, const char *url) +{ + return replace_index_url (content, url, "[[index]]", NULL); +} + +static char * +replace_python_install_mirror (const char *content, const char *url) +{ + return replace_key_value (content, "python-install-mirror", url, NULL); +} + +static char * +uvh_get_top_level_value (const char *content, const char *key) +{ + return uvh_get_value_in_table (content, key, NULL); +} diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c index 7a08d8c5..3eef96af 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv.c @@ -37,18 +37,23 @@ pl_python_uv_prelude (void) /** + * @consult: https://docs.astral.sh/uv/concepts/configuration-files * chsrc get uv * - * uv 配置文件查找优先级: - * 1 ./uv.toml (项目级,已实现) - * 2. $workspaces/pyproject.toml (项目级,未实现,因过于罕见不考虑适配) - * 3. ~/.config/uv/uv.toml (用户级,已实现) - * 4. /etc/uv/uv.toml (系统级,考虑到权限问题不予考虑) + * uv 配置文件选择顺序 (chsrc 写入策略): + * 1. ./uv.toml (项目级;与 pyproject.toml 并存时 uv 优先读取它) + * 2. ./pyproject.toml (项目级 [tool.uv] 段) + * 3. 用户级 uv.toml + * + * uv 还会向父目录查找最近的项目配置,并合并用户/系统配置;chsrc 为避免 + * 越界修改父项目,只管理当前目录或明确的用户级配置文件。 */ -#define PL_Python_uv_ConfigFile "uv.toml" -#define PL_Python_uv_Local_ConfigPath "./" -#define PL_Python_uv_User_ConfigPath "~/.config/uv/" +#define PL_Python_uv_ConfigFile "uv.toml" +#define PL_Python_uv_PyprojectConfigFile "pyproject.toml" +/* 注意: 不能以 "./" 开头 —— Windows 上 chsrc_backup 的 copy 命令无法处理 */ +#define PL_Python_uv_Local_ConfigPath "" +#define PL_Python_uv_User_ConfigPath "~/.config/uv/" char * @@ -56,6 +61,16 @@ pl_python_find_uv_config (bool mkdir) { if (chsrc_in_project_scope_mode()) { + /* uv.toml 与 pyproject.toml 并存时,uv 会忽略后者中的 [tool.uv]。 */ + char *uv_toml = xy_2strcat (PL_Python_uv_Local_ConfigPath, PL_Python_uv_ConfigFile); + if (xy_file_exist (uv_toml)) + return uv_toml; + free (uv_toml); + + char *pyproject = xy_2strcat (PL_Python_uv_Local_ConfigPath, PL_Python_uv_PyprojectConfigFile); + if (xy_file_exist (pyproject)) + return pyproject; + free (pyproject); return xy_2strcat (PL_Python_uv_Local_ConfigPath, PL_Python_uv_ConfigFile); } else @@ -77,6 +92,7 @@ pl_python_find_uv_config (bool mkdir) chsrc_ensure_dir (config_dir); } char *result = xy_2strcat (config_dir, PL_Python_uv_ConfigFile); + free (config_dir); return result; } else @@ -103,35 +119,50 @@ pl_python_uv_getsrc (char *option) chsrc_error2 ("无法获取 uv 配置文件路径"); else chsrc_error2 ("未找到 uv 配置文件"); + free (uv_config); return; } - /* 获取 [[index]] 配置项的 url */ - if (xy.on_windows) + /* uv.toml 与 pyproject.toml 均使用同一套受限 TOML 读取逻辑。 */ + char *content = xy_file_read (uv_config); + if (!content) + { + chsrc_error2 ("无法读取 uv 配置文件"); + free (uv_config); + return; + } + + bool pyproject = xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile); + const char *index_header = pyproject ? "[[tool.uv.index]]" : "[[index]]"; + const char *parent_table = pyproject ? "[tool.uv]" : NULL; + + char *url = uvh_get_index_url (content, index_header, parent_table); + if (url) { - /* 在 Windows 上使用 PowerShell 替代 grep */ - char *script = xy_str_gsub (RAWSTR_pl_python_get_uv_config_on_windows, "@f@", uv_config); - chsrc_run_as_powershell_file (script); + say (url); + free (url); } else { - /* 在类 Unix 系统上使用 grep */ - char *cmd = xy_str_gsub (RAWSTR_pl_python_get_uv_config, "@f@", uv_config); - chsrc_run (cmd, RunOpt_Default); + if (ENGLISH) + chsrc_note2 ("No source configured in uv, showing default upstream source:"); + else + chsrc_note2 ("uv 中未配置源,显示默认上游源:"); + Source_t default_source = chsrc_yield_source (&pl_python_group_target, "upstream"); + say (default_source.url); } - /* 检查 Python 下载镜像 (顶层 python-install-mirror 键) */ - char *content = xy_file_read (uv_config); - if (content) + char *mirror = pyproject + ? uvh_get_value_in_table (content, "python-install-mirror", parent_table) + : uvh_get_top_level_value (content, "python-install-mirror"); + if (mirror) { - char *mirror = uvh_get_top_level_value (content, "python-install-mirror"); - if (mirror) - { - say (mirror); - free (mirror); /* uvh_get_top_level_value 返回 caller-free */ - } - free (content); + say (mirror); + free (mirror); } + + free (content); + free (uv_config); } @@ -144,6 +175,14 @@ pl_python_uv_getsrc (char *option) def_target (pl_uv_github_release, NULL); +/* CNB (Cloud Native Build, 腾讯) 托管的 python-build-standalone releases 镜像 */ +static MirrorSite_t CnbPython = +{ + IS_DedicatedMirrorSite, + "cnb", "CNB", "Cloud Native Build (腾讯)", "https://cnb.cool/", + {NotSkip, NA, NA, "https://cnb.cool/astral-sh/python-build-standalone/-/releases/download/20260728/cpython-3.14.6+20260728-i686-pc-windows-msvc-install_only_stripped.tar.gz", ACCURATE} +}; + /* 内部 target, 无 CLI 入口, 以下为占位 */ void pl_uv_github_release_getsrc (char *o) { (void)o; } void pl_uv_github_release_setsrc (char *o) { (void)o; } @@ -167,12 +206,14 @@ pl_uv_github_release_prelude (void) {&UpstreamProvider, "https://github.com/astral-sh/python-build-standalone/releases/download", DelegateToUpstream}, {&Nju, "https://mirrors.nju.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude}, {&Ustc, "https://mirrors.ustc.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude}, - {&Lzuoss, "https://mirror.lzu.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude} + {&Lzuoss, "https://mirror.lzu.edu.cn/github-release/astral-sh/python-build-standalone", FeedByPrelude}, + {&CnbPython, "https://cnb.cool/astral-sh/python-build-standalone/-/releases/download", FeedByPrelude} def_sources_end () -#define GH_SM_POSTFIX "/20260510/cpython-3.14.5+20260510-i686-pc-windows-msvc-install_only_stripped.tar.gz" - chef_set_smURL_with_postfix (this, &Nju, GH_SM_POSTFIX); - chef_set_smURL_with_postfix (this, &Lzuoss, GH_SM_POSTFIX); +#define GH_SM_POSTFIX "/20260728/cpython-3.14.6+20260728-i686-pc-windows-msvc-install_only_stripped.tar.gz" + chef_set_smURL_with_postfix (this, &Nju, GH_SM_POSTFIX); + chef_set_smURL_with_postfix (this, &Lzuoss, GH_SM_POSTFIX); + chef_set_smURL_with_postfix (this, &CnbPython, GH_SM_POSTFIX); #undef GH_SM_POSTFIX /* 2026-5-31: USTC 仅保留 Latest, 只能用 SHA256SUMS 粗略测速 */ @@ -192,11 +233,25 @@ pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char char *content = xy_file_read (uv_config); if (!content) content = xy_strdup (""); - char *updated = replace_pypi_index_url (content, pypi_url); - - char *final = replace_python_install_mirror (updated, py_dl_url); + char *final = NULL; + if (xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile)) + { + /* pyproject.toml: 配置位于 [tool.uv] 表内 */ + char *updated = replace_index_url (content, pypi_url, "[[tool.uv.index]]", "[tool.uv]"); + final = replace_key_value (updated, "python-install-mirror", py_dl_url, "[tool.uv]"); + free (updated); + } + else + { + /* uv.toml: 配置位于顶层 */ + char *updated = replace_pypi_index_url (content, pypi_url); + final = replace_python_install_mirror (updated, py_dl_url); + free (updated); + } chsrc_overwrite_file (final, uv_config); + free (final); + free (content); } @@ -221,34 +276,32 @@ pl_python_uv_setsrc (char *option) return; } - /* reset: 清理 [[index]] 段 url 与 python-install-mirror 行 */ + /* reset: 把 index 与 python-install-mirror 写回默认上游 (PyPI 与 + * python-build-standalone), 不依赖 .bak 恢复旧 URL。 */ if (chsrc_in_reset_mode ()) { if (!chsrc_check_file (uv_config)) { chsrc_info ("没有 uv 配置文件, 无需重置"); + free (uv_config); return; } - /* 读内容, 清理, 写回 */ - char *content = xy_file_read (uv_config); - if (!content) - { - chsrc_error2 ("无法读取 uv 配置文件"); - return; - } + Source_t default_pypi = chsrc_yield_source (&pl_python_group_target, "upstream"); + Source_t default_gh = chsrc_yield_source (&pl_uv_github_release_target, "upstream"); - char *cleaned = cleanup_config_for_reset (content); - chsrc_overwrite_file (cleaned, uv_config); + chsrc_backup (uv_config); + pl_python_uv_write_all (uv_config, default_pypi.url, default_gh.url); + free (uv_config); return; } /* set: 选取源并写入 */ - Source_t source = chsrc_yield_source (&pl_python_group_target, option); - - /* 若 option 命中了 GitHub release 源则直接用, 否则自动测速 */ + char *pypi_opt = option; char *gh_opt = option; + + /* option 命中内部 gh 源 (如 cnb) 时: gh 直接用指定源, pypi 自动测速 */ if (gh_opt && !chsrc_in_reset_mode () && !hp_is_url (gh_opt)) { bool found = false; @@ -257,8 +310,13 @@ pl_python_uv_setsrc (char *option) for (int i = 0; i < pl_uv_github_release_target.sources_n; i++) if (xy_streql (pl_uv_github_release_target.sources[i].mirror->code, gh_opt)) { found = true; break; } - if (!found) gh_opt = NULL; + if (found) + pypi_opt = NULL; /* cnb 等是 gh 专用源, pypi 部分自动测速 */ + else + gh_opt = NULL; } + + Source_t source = chsrc_yield_source (&pl_python_group_target, pypi_opt); Source_t gh_source = chsrc_yield_source (&pl_uv_github_release_target, gh_opt); if (chsrc_in_standalone_mode()) @@ -266,6 +324,7 @@ pl_python_uv_setsrc (char *option) chsrc_backup (uv_config); pl_python_uv_write_all (uv_config, source.url, gh_source.url); + free (uv_config); if (chsrc_in_standalone_mode()) { diff --git a/test/uv-helper-test.c b/test/uv-helper-test.c index 7ff34c37..eb6da5d3 100644 --- a/test/uv-helper-test.c +++ b/test/uv-helper-test.c @@ -89,11 +89,11 @@ test_replace_pypi_index_url (void) CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n"); free (r); - /* 多个 [[index]] 段: 只修改第一个 */ + /* 多个 [[index]] 段: 优先修改 default = true 的 PyPI 替代源 */ r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://old1\"\n\n[[index]]\nurl = \"https://old2\"\n", + "[[index]]\nurl = \"https://private\"\ndefault = false\n\n[[index]]\nurl = \"https://old\"\ndefault = true\n", "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n\n[[index]]\nurl = \"https://old2\"\n"); + CHECK_STR (r, "[[index]]\nurl = \"https://private\"\ndefault = false\n\n[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); free (r); /* 段边界: [[index]] 之后其他表的 url 键不被误改 */ @@ -152,61 +152,6 @@ test_replace_python_install_mirror (void) } -static void -test_cleanup_config_for_reset (void) -{ - char *r; - - /* 空内容 */ - r = cleanup_config_for_reset (""); - CHECK_STR (r, ""); - free (r); - - /* 只清理 [[index]] 的 url/default 与顶层 python-install-mirror; - * 其他表 ([python]) 内的 url 键必须保留 */ - r = cleanup_config_for_reset ( - "python-install-mirror = \"https://old\"\n" - "[[index]]\n" - "name = \"tuna\"\n" - "url = \"https://old\"\n" - "default = true\n" - "\n" - "[python]\n" - "url = \"https://python\"\n"); - CHECK_STR (r, - "[[index]]\n" - "name = \"tuna\"\n" - "\n" - "[python]\n" - "url = \"https://python\"\n"); - free (r); - - /* 顶层其他键保留 */ - r = cleanup_config_for_reset ( - "a = 1\npython-install-mirror = \"https://old\"\nb = 2\n"); - CHECK_STR (r, "a = 1\nb = 2\n"); - free (r); - - /* 注释行保留 (注释中的 url 键不误删) */ - r = cleanup_config_for_reset ( - "# url = \"comment\"\n[[index]]\nurl = \"real\"\n"); - CHECK_STR (r, "# url = \"comment\"\n[[index]]\n"); - free (r); - - /* 多个 [[index]] 段都清理 */ - r = cleanup_config_for_reset ( - "[[index]]\nurl = \"https://old1\"\n\n[[index]]\nurl = \"https://old2\"\n"); - CHECK_STR (r, "[[index]]\n\n[[index]]\n"); - free (r); - - /* 其他表内的 default 键保留 */ - r = cleanup_config_for_reset ( - "[python]\ndefault = \"x\"\n"); - CHECK_STR (r, "[python]\ndefault = \"x\"\n"); - free (r); -} - - static void test_uvh_get_top_level_value (void) { @@ -245,13 +190,154 @@ test_uvh_get_top_level_value (void) } +static void +test_replace_index_url_pyproject (void) +{ + char *r; + + /* 空文件: 创建 [tool.uv] 表 + [[tool.uv.index]] 段 */ + r = replace_index_url ("", "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* 已有 [tool.uv] 无 index 段: 表内追加 (在下一个段头前) */ + r = replace_index_url ( + "[tool.uv]\nsome = 1\n[tool.poetry]\nx = 1\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\nsome = 1\n[tool.poetry]\nx = 1\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* 已有 [tool.uv] + [[tool.uv.index]]: 替换 url */ + r = replace_index_url ( + "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://old\"\ndefault = true\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); + + /* 有 [project] 等其他表时正确找到 [tool.uv] */ + r = replace_index_url ( + "[project]\nname = \"x\"\n\n[tool.uv]\n[[tool.uv.index]]\nurl = \"https://old\"\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, "[project]\nname = \"x\"\n\n[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\n"); + free (r); + + /* [[tool.uv.index]] 段内无 url 键: 段内插入 url + default */ + r = replace_index_url ( + "[tool.uv]\n[[tool.uv.index]]\nname = \"mirror\"\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\nname = \"mirror\"\n"); + free (r); + + /* CRLF 表头与缩进键应被识别,不能产生 duplicate table/key */ + r = replace_index_url ( + " [tool.uv]\r\n python-install-mirror = \"https://old-python\"\r\n [[tool.uv.index]]\r\n url = \"https://old\"\r\n default = true\r\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, + " [tool.uv]\r\n python-install-mirror = \"https://old-python\"\r\n [[tool.uv.index]]\r\n url = \"https://mirror\"\r\n default = true\r\n"); + free (r); + + /* CRLF 文件中无 [tool.uv]: 新建段同样使用 CRLF */ + r = replace_index_url ( + "[project]\r\nname = \"x\"\r\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, + "[project]\r\nname = \"x\"\r\n\r\n[tool.uv]\r\n[[tool.uv.index]]\r\nurl = \"https://mirror\"\r\ndefault = true\r\n"); + free (r); + + /* [tool.uv.sources] 不会阻止后续 index 被找到 */ + r = replace_index_url ( + "[tool.uv]\n[tool.uv.sources]\nfoo = { path = \"foo\" }\n[[tool.uv.index]]\nurl = \"https://old\"\ndefault = true\n", + "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); + CHECK_STR (r, + "[tool.uv]\n[tool.uv.sources]\nfoo = { path = \"foo\" }\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); + free (r); +} + + +static void +test_replace_key_value_pyproject (void) +{ + char *r; + + /* 空文件: 创建 [tool.uv] 表 + 键 */ + r = replace_key_value ("", "python-install-mirror", "https://mirror", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n"); + free (r); + + /* [tool.uv] 内已有键: 替换 */ + r = replace_key_value ( + "[tool.uv]\npython-install-mirror = \"https://old\"\n", + "python-install-mirror", "https://mirror", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n"); + free (r); + + /* [tool.uv] 内无该键但已有 [[tool.uv.index]] 子表: 键插在子表头前 */ + r = replace_key_value ( + "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://pypi\"\n", + "python-install-mirror", "https://mirror", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n[[tool.uv.index]]\nurl = \"https://pypi\"\n"); + free (r); + + /* 缩进键就地替换,不重复定义 */ + r = replace_key_value ( + "[tool.uv]\r\n python-install-mirror = \"https://old\"\r\n", + "python-install-mirror", "https://mirror", "[tool.uv]"); + CHECK_STR (r, "[tool.uv]\r\n python-install-mirror = \"https://mirror\"\r\n"); + free (r); + + /* CRLF 文件中无 [tool.uv]: 新建表同样使用 CRLF */ + r = replace_key_value ( + "[project]\r\nname = \"x\"\r\n", + "python-install-mirror", "https://mirror", "[tool.uv]"); + CHECK_STR (r, + "[project]\r\nname = \"x\"\r\n\r\n[tool.uv]\r\npython-install-mirror = \"https://mirror\"\r\n"); + free (r); +} + + +static void +test_uvh_get_value_pyproject (void) +{ + char *v; + + /* 表内键提取 */ + v = uvh_get_value_in_table ( + "[tool.uv]\npython-install-mirror = \"https://x\"\n", "python-install-mirror", "[tool.uv]"); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://x"); free (v); } + + /* 其他表内同名键: 找不到 */ + v = uvh_get_value_in_table ( + "[tool.other]\npython-install-mirror = \"https://x\"\n", "python-install-mirror", "[tool.uv]"); + CHECK (v == NULL); + + /* index url 提取 */ + v = uvh_get_index_url ( + "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://pypi\"\ndefault = true\n", + "[[tool.uv.index]]", "[tool.uv]"); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://pypi"); free (v); } + + /* 无 index 段: NULL */ + v = uvh_get_index_url ( + "[tool.uv]\nsome = 1\n", "[[tool.uv.index]]", "[tool.uv]"); + CHECK (v == NULL); + + /* 表不存在: NULL */ + v = uvh_get_index_url ("", "[[tool.uv.index]]", "[tool.uv]"); + CHECK (v == NULL); +} + + int main (void) { test_replace_pypi_index_url (); test_replace_python_install_mirror (); - test_cleanup_config_for_reset (); test_uvh_get_top_level_value (); + test_replace_index_url_pyproject (); + test_replace_key_value_pyproject (); + test_uvh_get_value_pyproject (); if (failures) { From 712b47f1c131fab518067d883edcaa32e813d025 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 22:29:07 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E5=BF=BD=E7=95=A5=20.pdb=20=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E7=AC=A6=E5=8F=B7=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2f9e20a2..668f68f0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ *.out *.exe *.res +*.pdb ############################## From bd26fb66f10efafe8bb7eb9817642e00c74d0a63 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 22:39:25 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=20uv=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=94=B9=E5=86=99=E5=99=A8=E7=9A=84=E8=BD=AC=E4=B9=89?= =?UTF-8?q?=E4=B8=8E=E5=B8=A6=E5=BC=95=E5=8F=B7=E9=94=AE=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0edge=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\350\257\264\346\230\216.md" | 43 +- src/recipe/lang/Python/uv-helper.c | 480 +++++++++++++++--- src/recipe/lang/Python/uv.c | 143 ++++-- test/uv-helper-test.c | 41 ++ 4 files changed, 579 insertions(+), 128 deletions(-) diff --git "a/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" "b/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" index a310d2cb..f938f57f 100644 --- "a/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" +++ "b/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" @@ -16,7 +16,6 @@
- ## 配置文件选择 项目作用域只修改当前目录,不向父目录查找并修改配置: @@ -25,66 +24,48 @@ 2. 否则,当前目录存在 `pyproject.toml` 时修改其中的 `[tool.uv]` 3. 两者都不存在时创建 `uv.toml` -这个顺序与 uv 的行为一致:同一目录同时存在 `uv.toml` 与 -`pyproject.toml` 的 `[tool.uv]` 时,uv 优先使用 `uv.toml` 中的对应字段。 +这个顺序与 uv 的行为一致:同一目录同时存在 `uv.toml` 与 `pyproject.toml` 的 `[tool.uv]` 时,uv 优先使用 `uv.toml` 中的对应字段。 -用户作用域使用 uv 的用户配置目录:Windows 为 `%APPDATA%\uv\uv.toml`, -Linux 和 macOS 为 `~/.config/uv/uv.toml`。 +用户作用域使用 uv 的用户配置目录:Windows 为 `%APPDATA%\uv\uv.toml`,Linux 和 macOS 优先使用 `$XDG_CONFIG_HOME/uv/uv.toml`,未设置时使用 `~/.config/uv/uv.toml`。
- ## Python 包索引 -索引 URL 必须指向兼容 Python Simple Repository API 的地址,通常以 -`/simple` 结尾;uv 不要求 URL 必须以 `/` 结尾。 +索引 URL 必须指向兼容 Python Simple Repository API 的地址,通常以 `/simple` 结尾;uv 不要求 URL 必须以 `/` 结尾。 -`default = true` 表示该 index 替代 uv 内置的 PyPI,并且始终具有最低 -index 优先级。它不表示“文本中的第一个”或“最高优先级”。配置中存在多个 -index 时,chsrc 优先更新 `default = true` 的 index;没有 default index -时才更新第一个 index。 +`default = true` 表示该 index 替代 uv 内置的 PyPI,并且始终具有最低 index 优先级。它不表示“文本中的第一个”或“最高优先级”。配置中存在多个 index 时,chsrc 优先更新 `default = true` 的 index;没有 default index 时才更新第一个 index。 -默认的 `first-index` 策略会在第一个包含目标包的 index 停止搜索。私有源 -与公共镜像并存时,不应把它理解成跨所有 index 选择全局最新版本。 +默认的 `first-index` 策略会在第一个包含目标包的 index 停止搜索。私有源与公共镜像并存时,不应把它理解成跨所有 index 选择全局最新版本。
- ## Python 下载镜像 -`python-install-mirror` 是 -`https://github.com/astral-sh/python-build-standalone/releases/download` -的替换前缀。配置值必须是镜像侧对应的 `releases/download` 基址,不能是 -仓库主页、某个具体构建日期或某个归档文件。uv 会在其后追加构建日期和 +`python-install-mirror` 是 `https://github.com/astral-sh/python-build-standalone/releases/download` 的替换前缀。配置值必须是镜像侧对应的 `releases/download` 基址,不能是仓库主页、某个具体构建日期或某个归档文件。uv 会在其后追加构建日期和 归档文件名。 官方示例使用不带尾部 `/` 的基址,chsrc 内置源也遵循该形式。
- ## reset 与备份 `chsrc set uv` 修改已有配置前会创建 `.bak` 备份。 -`chsrc reset uv` 把 chsrc 管理的 index 与 `python-install-mirror` 写回 -默认上游:index 恢复为 `https://pypi.org/simple`, -`python-install-mirror` 恢复为 -`https://github.com/astral-sh/python-build-standalone/releases/download`。 +`chsrc reset uv` 把 chsrc 管理的 index 与 `python-install-mirror` 写回默认上游:index 恢复为 `https://pypi.org/simple`, +`python-install-mirror` 恢复为 `https://github.com/astral-sh/python-build-standalone/releases/download`。 其他用户配置原样保留,不依赖 `.bak` 恢复旧 URL。 -`default = true` 的 index(不存在时取第一个)被视为 PyPI 替代源, -reset 只修改这一处;私有 index 不受影响。 +指定镜像代码时,若该代码同时提供 PyPI 与 Python 下载镜像(例如 `nju`),两部分使用同一镜像代码;仅提供其中一类时,另一类自动测速。用户自定义 URL 只解释为 PyPI index URL,因为 Python 下载镜像要求完全不同的 releases 目录布局。 -
+`default = true` 的 index(不存在时取第一个)被视为 PyPI 替代源,reset 只修改这一处;私有 index 不受影响。 +
## 实现边界 -配置改写器只解析 chsrc 使用的 TOML 子集,但支持常见的行首缩进、CRLF、 -行尾注释、多个 index,以及位于 `[tool.uv.sources]` 等子表之后的 -`[[tool.uv.index]]`。改写会保留原有缩进与行尾风格(CRLF/LF)。多行字符串 -和完整 TOML 转义语义不属于该轻量改写器的支持范围。 +配置改写器只解析 chsrc 使用的 TOML 子集,但支持常见的行首缩进、CRLF、行尾注释、多个 index,以及位于 `[tool.uv.sources]` 等子表之后的 `[[tool.uv.index]]`。改写会保留原有缩进与行尾风格(CRLF/LF)。多行字符串和完整 TOML 转义语义不属于该轻量改写器的支持范围。 参考: diff --git a/src/recipe/lang/Python/uv-helper.c b/src/recipe/lang/Python/uv-helper.c index 4a6b84de..12f06abc 100644 --- a/src/recipe/lang/Python/uv-helper.c +++ b/src/recipe/lang/Python/uv-helper.c @@ -8,8 +8,9 @@ * Last Modified : <2026-08-02> * * - * uv 配置 (uv.toml / pyproject.toml) 字符串改写工具 - * (由 uv.c #include, 可被 test 单独 #include) + * @file uv-helper.c + * @brief uv 配置 (uv.toml / pyproject.toml) 字符串改写工具 + * (由 uv.c #include, 可被 test 单独 #include) * * 仅处理 chsrc 关心的 uv 配置子集, 不是完整 TOML 解析器: * - uv.toml: 顶层 [[index]] 数组表、顶层 python-install-mirror 键 @@ -17,8 +18,8 @@ * python-install-mirror 键 * * 刻意不处理: 多行字符串、数组/内联表值、字符串转义 —— - * 因为所有改写都按"整行替换/删除"进行, 无需解析值本身。 - * 已知限制: 多行字符串 (""") 内若出现行首 '[' 会被误认为段头, 罕见可忽略。 + * 因为所有改写都按“整行替换/删除”进行, 无需解析值本身。 + * 已知限制: 多行字符串 (`"""`) 内若出现行首 `[` 会被误认为段头, 罕见可忽略。 * * 所有函数均为 static, 依赖调用方所在翻译单元提供 stdlib/string.h。 * ------------------------------------------------------------*/ @@ -29,18 +30,43 @@ #include /** - * 判断行首 (调用方必须保证 p 位于行首) 是否以 key 作为键开始。 - * 键名后必须紧跟 =、空白或行尾, 避免误匹配 urlx 之类前缀键。 + * @brief 判断行首是否以 key 作为键开始。 + * + * 支持 bare key 与单/双引号 quoted key (如 "url" 或 'url')。 + * 调用方必须保证 p 位于行首。键名后必须紧跟 =、空白或行尾, + * 避免误匹配 urlx 之类前缀键。 + * + * @param p 待检查的字符串, 必须位于行首 + * @param key 键名 + * + * @return 行首以 key 开始且满足键边界时为 true, 否则为 false */ static bool uvh_key_prefix (const char *p, const char *key) { size_t kl = strlen (key); - if (strncmp (p, key, kl) != 0) return false; - char c = p[kl]; + const char *end = NULL; + if (strncmp (p, key, kl) == 0) + end = p + kl; + else if ((*p == '"' || *p == '\'') + && strncmp (p + 1, key, kl) == 0 && p[kl + 1] == *p) + end = p + kl + 2; + else + return false; + + char c = *end; return c == '=' || c == ' ' || c == '\t' || c == '\n' || c == '\0'; } +/** + * @brief 跳过行首的空白字符 (空格与 Tab), 返回首个非空白字符位置。 + * + * 不修改原字符串, 返回的是原字符串内部的指针。 + * + * @param p 输入字符串 + * + * @return 原字符串中第一个非空白字符的指针 + */ static const char * uvh_skip_indent (const char *p) { @@ -49,20 +75,45 @@ uvh_skip_indent (const char *p) } /** - * 判断行首 (调用方必须保证 p 位于行首) 是否为指定表头文本。 - * 允许缩进、CRLF 和行尾注释。 + * @brief 判断行首是否为指定表头文本。 + * + * 允许缩进、CRLF 和行尾注释; 额外把 [tool."uv"] / [tool.'uv'] 识别为 + * [tool.uv] 的等价形式。调用方必须保证 p 位于行首。 + * + * @param p 待检查的字符串, 必须位于行首 + * @param header 表头文本, 如 "[[index]]" 或 "[tool.uv]" + * + * @return 匹配时返回 true, 否则返回 false */ static bool uvh_header_match (const char *p, const char *header) { p = uvh_skip_indent (p); size_t hl = strlen (header); - if (strncmp (p, header, hl) != 0) return false; - char c = p[hl]; - return c == '\n' || c == '\r' || c == '\0' || c == ' ' || c == '\t' || c == '#'; + if (strncmp (p, header, hl) == 0) + { + char c = p[hl]; + return c == '\n' || c == '\r' || c == '\0' || c == ' ' || c == '\t' || c == '#'; + } + if (strcmp (header, "[tool.uv]") == 0 + && (strncmp (p, "[tool.\"uv\"]", 11) == 0 + || strncmp (p, "[tool.'uv']", 11) == 0)) + { + char c = p[11]; + return c == '\n' || c == '\r' || c == '\0' || c == ' ' || c == '\t' || c == '#'; + } + return false; } -/* 跳过当前行 (含换行符), 返回下一行的行首 */ +/** + * @brief 跳过当前行 (含换行符), 返回下一行的行首。 + * + * 若当前行是最后一行 (无换行符), 返回指向字符串结尾 '\0' 的指针。 + * + * @param p 当前行的行首 + * + * @return 下一行的行首指针 (或字符串结尾) + */ static const char * uvh_next_line (const char *p) { @@ -72,8 +123,11 @@ uvh_next_line (const char *p) } /** - * 从 start 开始找下一个段头行 (行首 '[') 的位置。 - * 返回 NULL 表示直到文件结尾都没有新段头。 + * @brief 从 start 开始查找下一个段头行 (行首 '[') 的位置。 + * + * @param start 搜索起点 + * + * @return 下一个段头行的位置; 直到文件结尾都没有新段头时返回 NULL */ static const char * uvh_find_section_end (const char *start) @@ -88,7 +142,12 @@ uvh_find_section_end (const char *start) } /** - * 全文件查找第一个行首匹配 header 的表头位置; 未找到返回 NULL。 + * @brief 全文件查找第一个行首匹配 header 的表头位置。 + * + * @param content 文件内容 + * @param header 要查找的表头文本 + * + * @return 匹配的表头行位置; 未找到返回 NULL */ static const char * uvh_find_table (const char *content, const char *header) @@ -104,7 +163,13 @@ uvh_find_table (const char *content, const char *header) } /** - * 在 [first, end) 行区间内查找以 key 开头的键行; end 为 NULL 时到文件尾。 + * @brief 在 [first, end) 行区间内查找以 key 开头的键行。 + * + * @param first 区间起点 (行首) + * @param end 区间终点 (不含); 为 NULL 时搜索到文件结尾 + * @param key 键名 + * + * @return 匹配的键行位置; 未找到返回 NULL */ static const char * uvh_find_key_in_section (const char *first, const char *end, const char *key) @@ -117,8 +182,121 @@ uvh_find_key_in_section (const char *first, const char *end, const char *key) } /** - * 从 "key = \"value\"" 或 "key = 'value'" 行提取字符串值。 - * @return malloc 的 caller-free 字符串; 值不是字符串时返回 NULL。 + * @brief 将字符串转义为 TOML basic string 的双引号内容。 + * + * 对反斜杠、双引号与 \b \t \n \f \r 进行转义, 其余字符原样保留; + * 返回值直接放入 "..." 中即为合法 TOML 字符串。 + * + * @param value 原始字符串 + * + * @return malloc 的 caller-free 转义后字符串 + * + * @memory SAFE + * return caller-free + */ +static char * +uvh_escape_basic_string (const char *value) +{ + size_t n = 0; + for (const char *p = value; *p; p++) + { + switch (*p) + { + case '\\': case '"': case '\b': case '\t': case '\n': + case '\f': case '\r': n += 2; break; + default: n++; + } + } + + char *ret = malloc (n + 1); + size_t pos = 0; + for (const char *p = value; *p; p++) + { + switch (*p) + { + case '\\': ret[pos++] = '\\'; ret[pos++] = '\\'; break; + case '"': ret[pos++] = '\\'; ret[pos++] = '"'; break; + case '\b': ret[pos++] = '\\'; ret[pos++] = 'b'; break; + case '\t': ret[pos++] = '\\'; ret[pos++] = 't'; break; + case '\n': ret[pos++] = '\\'; ret[pos++] = 'n'; break; + case '\f': ret[pos++] = '\\'; ret[pos++] = 'f'; break; + case '\r': ret[pos++] = '\\'; ret[pos++] = 'r'; break; + default: ret[pos++] = *p; + } + } + ret[pos] = '\0'; + return ret; +} + +/** + * @brief 将单个十六进制字符转换为数值。 + * + * @param c 十六进制字符 (0-9, a-f, A-F) + * + * @return 对应的数值 0-15; 不是合法十六进制字符时返回 -1 + */ +static int +uvh_hex_digit (char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/** + * @brief 将 Unicode code point 编码为 UTF-8 并追加到输出缓冲区。 + * + * 编码后为 1~4 字节; 代理区 (U+D800-U+DFFF) 与超出 U+10FFFF 的 + * 无效 code point 返回 false 且不写入任何字节。 + * + * @param out 输出缓冲区 (调用方保证足够容纳 4 字节) + * @param pos 输出缓冲区的当前写入位置 (会递增) + * @param codepoint Unicode code point + * + * @return 编码成功返回 true, 无效 code point 返回 false + */ +static bool +uvh_append_utf8 (char **out, size_t *pos, unsigned long codepoint) +{ + if (codepoint <= 0x7f) + (*out)[(*pos)++] = (char)codepoint; + else if (codepoint <= 0x7ff) + { + (*out)[(*pos)++] = (char)(0xc0 | (codepoint >> 6)); + (*out)[(*pos)++] = (char)(0x80 | (codepoint & 0x3f)); + } + else if (codepoint <= 0xffff) + { + if (codepoint >= 0xd800 && codepoint <= 0xdfff) return false; + (*out)[(*pos)++] = (char)(0xe0 | (codepoint >> 12)); + (*out)[(*pos)++] = (char)(0x80 | ((codepoint >> 6) & 0x3f)); + (*out)[(*pos)++] = (char)(0x80 | (codepoint & 0x3f)); + } + else if (codepoint <= 0x10ffff) + { + (*out)[(*pos)++] = (char)(0xf0 | (codepoint >> 18)); + (*out)[(*pos)++] = (char)(0x80 | ((codepoint >> 12) & 0x3f)); + (*out)[(*pos)++] = (char)(0x80 | ((codepoint >> 6) & 0x3f)); + (*out)[(*pos)++] = (char)(0x80 | (codepoint & 0x3f)); + } + else return false; + return true; +} + +/** + * @brief 从 "key = \"value\"" 或 "key = 'value'" 行提取字符串值。 + * + * 支持 TOML basic string 的常见转义 (\", \\, \b, \t, \n, \f, \r, + * \uXXXX, \UXXXXXXXX); literal string (单引号) 不处理转义。 + * 未闭合字符串、非法转义或值不是字符串时返回 NULL。 + * + * @param line 包含 "key = value" 的行 + * + * @return malloc 的 caller-free 解码后字符串; 失败返回 NULL + * + * @memory SAFE + * return caller-free */ static char * uvh_extract_string_value (const char *line) @@ -128,17 +306,71 @@ uvh_extract_string_value (const char *line) const char *v = eq + 1; while (*v == ' ' || *v == '\t') v++; if (*v != '"' && *v != '\'') return NULL; - char quote = *v; - v++; - const char *val = v; - while (*v && *v != quote && *v != '\n') v++; - size_t vl = v - val; - char *ret = malloc (vl + 1); - memcpy (ret, val, vl); - ret[vl] = '\0'; + char quote = *v++; + + size_t max_len = strcspn (v, "\n") + 1; + char *ret = malloc (max_len * 4); + size_t pos = 0; + bool closed = false; + while (*v && *v != '\n') + { + if (*v == quote) + { + closed = true; + break; + } + if (quote == '"' && *v == '\\') + { + v++; + if (*v == 'u' || *v == 'U') + { + int digits = (*v == 'u') ? 4 : 8; + unsigned long codepoint = 0; + v++; + for (int i = 0; i < digits; i++, v++) + { + int digit = uvh_hex_digit (*v); + if (digit < 0) { free (ret); return NULL; } + codepoint = (codepoint << 4) | (unsigned)digit; + } + if (!uvh_append_utf8 (&ret, &pos, codepoint)) + { free (ret); return NULL; } + continue; + } + switch (*v) + { + case '"': ret[pos++] = '"'; break; + case '\\': ret[pos++] = '\\'; break; + case 'b': ret[pos++] = '\b'; break; + case 't': ret[pos++] = '\t'; break; + case 'n': ret[pos++] = '\n'; break; + case 'f': ret[pos++] = '\f'; break; + case 'r': ret[pos++] = '\r'; break; + default: free (ret); return NULL; + } + if (*v) v++; + continue; + } + ret[pos++] = *v++; + } + if (!closed) + { + free (ret); + return NULL; + } + ret[pos] = '\0'; return ret; } +/** + * @brief 判断行中布尔值是否为 true。 + * + * 仅当值为小写 true 且后随空白、注释、行尾或结尾时返回 true。 + * + * @param line 包含 "key = value" 的行 + * + * @return 值为 true 时返回 true, 否则返回 false + */ static bool uvh_value_is_true (const char *line) { @@ -150,8 +382,15 @@ uvh_value_is_true (const char *line) } /** - * 查找要管理的 index。多个 index 中优先选择 default = true, - * 否则选择第一个;表可以位于 [tool.uv.sources] 等子表之后。 + * @brief 查找要管理的 index 段。 + * + * 多个 index 中优先选择 default = true 的段, 不存在 default 时回退到 + * 第一个; 段可以位于 [tool.uv.sources] 等子表之后。 + * + * @param content 文件内容 + * @param index_header index 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" + * + * @return 选中的 index 段头位置; 不存在 index 段时返回 NULL */ static const char * uvh_find_managed_index (const char *content, const char *index_header) @@ -170,7 +409,15 @@ uvh_find_managed_index (const char *content, const char *index_header) return first; } -/* 文件是否使用 CRLF 换行。 */ +/** + * @brief 检测文件内容是否使用 CRLF 换行。 + * + * 以第一个换行符前是否有 '\r' 判断; 混合换行风格时结果取决于首个换行。 + * + * @param content 文件内容 + * + * @return 使用 CRLF 时返回 true, 否则返回 false + */ static bool uvh_detect_crlf (const char *content) { @@ -179,10 +426,18 @@ uvh_detect_crlf (const char *content) } /** - * 按 content 的换行风格转换 str 中的 LF;content 为 CRLF 时返回 CRLF 版本。 - * 仅用于追加的纯 LF 文本,不要传入已含 CRLF 的 content 本身。 + * @brief 按 content 的换行风格转换 str 中的 LF。 + * + * content 为 CRLF 时返回 CRLF 版本, 否则原样返回。仅用于追加的纯 LF 文本, + * 不要传入已含 CRLF 的 content 本身。 + * + * @param content 用于判断换行风格的内容 + * @param str 纯 LF 的待转换文本 + * + * @return malloc 的 caller-free 转换后字符串 * - * @return caller-free + * @memory SAFE + * return caller-free */ static char * uvh_apply_eol (const char *content, const char *str) @@ -201,10 +456,18 @@ uvh_apply_eol (const char *content, const char *str) } /** - * 用 new_line_text (不含换行) 替换 content 中从 line 开始的整行, - * 保留原行缩进、行尾风格 (CRLF/LF) 与后续内容。 + * @brief 用 new_line_text 替换 content 中从 line 开始的整行。 * - * @return 新内容 (caller-free) + * 保留原行缩进、行尾风格 (CRLF/LF) 与后续内容; new_line_text 不含换行。 + * + * @param content 原始内容 + * @param line content 中待替换行的行首指针 + * @param new_line_text 新行文本 (不含换行) + * + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free */ static char * uvh_replace_line (const char *content, const char *line, const char *new_line_text) @@ -231,10 +494,19 @@ uvh_replace_line (const char *content, const char *line, const char *new_line_te } /** - * 在 insert_at 位置之前插入 insert_text (以 '\n' 结尾)。 - * 内容为 CRLF 时,插入文本与分隔换行同样转换为 CRLF。 + * @brief 在 insert_at 位置之前插入 insert_text (以 '\n' 结尾)。 * - * @return 新内容 (caller-free) + * 内容为 CRLF 时, 插入文本与分隔换行同样转换为 CRLF; insert_at 之前 + * 没有换行时会先补一个分隔换行。 + * + * @param content 原始内容 + * @param insert_at content 内的插入位置指针 + * @param insert_text 以 '\n' 结尾的待插入文本 + * + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free */ static char * uvh_insert_before (const char *content, const char *insert_at, const char *insert_text) @@ -267,7 +539,10 @@ uvh_insert_before (const char *content, const char *insert_at, const char *inser /** - * 替换/创建 index 数组表 (如 [[index]] 或 [[tool.uv.index]]) 的 url 值。 + * @brief 替换/创建 index 数组表 (如 [[index]] 或 [[tool.uv.index]]) 的 url 值。 + * + * 优先修改 default = true 的 index (uv 的 PyPI 替代源), 不存在时回退到 + * 第一个; 作用域内无 index 段时新建, 父表不存在时一并创建。 * * @param content 原始内容 * @param url 新的索引 URL @@ -275,7 +550,10 @@ uvh_insert_before (const char *content, const char *insert_at, const char *inser * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; * 父表不存在时会在文件末尾创建父表并写入 index 段 * - * @return 新内容 (caller-free) + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free */ static char * replace_index_url (const char *content, const char *url, const char *index_header, const char *parent_table) @@ -292,10 +570,12 @@ replace_index_url (const char *content, const char *url, const char *index_heade * 换行风格与 content 保持一致 */ size_t clen = strlen (content); const char *sep = (clen > 0) ? "\n" : ""; - size_t seg_len = strlen (parent_table) + strlen (index_header) + strlen (url) + 48; + char *escaped_url = uvh_escape_basic_string (url); + size_t seg_len = strlen (parent_table) + strlen (index_header) + strlen (escaped_url) + 48; char *seg = calloc (seg_len, 1); snprintf (seg, seg_len, "%s\n%s\nurl = \"%s\"\ndefault = true\n", - parent_table, index_header, url); + parent_table, index_header, escaped_url); + free (escaped_url); char *seg_eol = uvh_apply_eol (content, seg); free (seg); char *sep_eol = uvh_apply_eol (content, sep); @@ -311,9 +591,11 @@ replace_index_url (const char *content, const char *url, const char *index_heade /* 作用域内无 index 段: 追加到边界之前 */ if (!ih) { - size_t seglen = strlen (index_header) + strlen (url) + 48; + char *escaped_url = uvh_escape_basic_string (url); + size_t seglen = strlen (index_header) + strlen (escaped_url) + 48; char *seg = malloc (seglen); - snprintf (seg, seglen, "%s\nurl = \"%s\"\ndefault = true\n", index_header, url); + snprintf (seg, seglen, "%s\nurl = \"%s\"\ndefault = true\n", index_header, escaped_url); + free (escaped_url); char *ret = uvh_insert_before (content, boundary, seg); free (seg); return ret; @@ -328,37 +610,46 @@ replace_index_url (const char *content, const char *url, const char *index_heade if (!url_line) { bool has_default = uvh_find_key_in_section (first, end, "default") != NULL; - size_t seglen = strlen (url) + (has_default ? 24 : 48); + char *escaped_url = uvh_escape_basic_string (url); + size_t seglen = strlen (escaped_url) + (has_default ? 24 : 48); char *seg = malloc (seglen); size_t spos = 0; - spos += snprintf (seg + spos, seglen - spos, "url = \"%s\"\n", url); + spos += snprintf (seg + spos, seglen - spos, "url = \"%s\"\n", escaped_url); if (!has_default) spos += snprintf (seg + spos, seglen - spos, "default = true\n"); char *ret = uvh_insert_before (content, first, seg); free (seg); + free (escaped_url); return ret; } /* 替换 url 行 */ - size_t newlen = strlen (url) + 32; + char *escaped_url = uvh_escape_basic_string (url); + size_t newlen = strlen (escaped_url) + 32; char *new_line = malloc (newlen); - snprintf (new_line, newlen, "url = \"%s\"", url); + snprintf (new_line, newlen, "url = \"%s\"", escaped_url); char *ret = uvh_replace_line (content, url_line, new_line); free (new_line); + free (escaped_url); return ret; } /** - * 替换/创建顶层键或指定表内键 (如 python-install-mirror) 的字符串值。 + * @brief 替换/创建顶层键或指定表内键 (如 python-install-mirror) 的字符串值。 * - * @param content 原始内容 - * @param key 键名 - * @param url 新值 + * 键已存在时原地替换, 不存在时追加到作用域末尾; 父表不存在时一并创建。 + * + * @param content 原始内容 + * @param key 键名 + * @param url 新值 * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; * 父表不存在时会在文件末尾创建父表并写入键 * - * @return 新内容 (caller-free) + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free */ static char * replace_key_value (const char *content, const char *key, const char *url, const char *parent_table) @@ -375,9 +666,11 @@ replace_key_value (const char *content, const char *key, const char *url, const * 换行风格与 content 保持一致 */ size_t clen = strlen (content); const char *sep = (clen > 0) ? "\n" : ""; - size_t seg_len = strlen (parent_table) + strlen (key) + strlen (url) + 16; + char *escaped_url = uvh_escape_basic_string (url); + size_t seg_len = strlen (parent_table) + strlen (key) + strlen (escaped_url) + 16; char *seg = calloc (seg_len, 1); - snprintf (seg, seg_len, "%s\n%s = \"%s\"\n", parent_table, key, url); + snprintf (seg, seg_len, "%s\n%s = \"%s\"\n", parent_table, key, escaped_url); + free (escaped_url); char *seg_eol = uvh_apply_eol (content, seg); free (seg); char *sep_eol = uvh_apply_eol (content, sep); @@ -419,28 +712,41 @@ replace_key_value (const char *content, const char *key, const char *url, const if (old_line) { /* 替换旧值行 */ - size_t newlen = strlen (key) + strlen (url) + 32; + char *escaped_url = uvh_escape_basic_string (url); + size_t newlen = strlen (key) + strlen (escaped_url) + 32; char *new_line = malloc (newlen); - snprintf (new_line, newlen, "%s = \"%s\"", key, url); + snprintf (new_line, newlen, "%s = \"%s\"", key, escaped_url); char *ret = uvh_replace_line (content, old_line, new_line); free (new_line); + free (escaped_url); return ret; } /* 键不存在: 追加到作用域末尾 */ - size_t seglen = strlen (key) + strlen (url) + 32; + char *escaped_url = uvh_escape_basic_string (url); + size_t seglen = strlen (key) + strlen (escaped_url) + 32; char *seg = malloc (seglen); - snprintf (seg, seglen, "%s = \"%s\"\n", key, url); + snprintf (seg, seglen, "%s = \"%s\"\n", key, escaped_url); char *ret = uvh_insert_before (content, insert_at, seg); free (seg); + free (escaped_url); return ret; } /** - * 在指定表 (parent_table 为 NULL 表示顶层) 内提取键的字符串值。 + * @brief 在指定表 (parent_table 为 NULL 表示顶层) 内提取键的字符串值。 + * + * 搜索范围限制在该表到下一个段头之间。 * - * @return malloc 的 caller-free 字符串; 未找到返回 NULL。 + * @param content 文件内容 + * @param key 键名 + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]" + * + * @return malloc 的 caller-free 解码后字符串; 未找到返回 NULL + * + * @memory SAFE + * return caller-free */ static char * uvh_get_value_in_table (const char *content, const char *key, const char *parent_table) @@ -463,18 +769,25 @@ uvh_get_value_in_table (const char *content, const char *key, const char *parent /** - * 提取 chsrc 管理的 index URL:优先 default = true,否则取第一个。 + * @brief 提取 chsrc 管理的 index URL。 + * + * 优先 default = true 的 index, 否则取第一个; fully-qualified + * [[tool.uv.index]] 会隐式创建父路径, 不要求显式 [tool.uv]。 * + * @param content 文件内容 * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]" * - * @return malloc 的 caller-free 字符串; 未找到返回 NULL。 + * @return malloc 的 caller-free 解码后 URL; 未找到返回 NULL + * + * @memory SAFE + * return caller-free */ static char * uvh_get_index_url (const char *content, const char *index_header, const char *parent_table) { - if (parent_table && !uvh_find_table (content, parent_table)) return NULL; - + /* fully-qualified [[tool.uv.index]] 会隐式创建父路径,不要求显式 [tool.uv]。 */ + (void)parent_table; const char *ih = uvh_find_managed_index (content, index_header); if (!ih) return NULL; @@ -488,18 +801,51 @@ uvh_get_index_url (const char *content, const char *index_header, const char *pa /* ============ uv.toml 便捷包装 (保持原函数名) ============ */ +/** + * @brief uv.toml 便捷包装: 替换/创建顶层 [[index]] 段的 url。 + * + * @param content 原始内容 + * @param url 新的索引 URL + * + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free + */ static char * replace_pypi_index_url (const char *content, const char *url) { return replace_index_url (content, url, "[[index]]", NULL); } +/** + * @brief uv.toml 便捷包装: 替换/创建顶层 python-install-mirror 键。 + * + * @param content 原始内容 + * @param url 新的 Python 下载镜像基址 + * + * @return malloc 的 caller-free 新内容 + * + * @memory SAFE + * return caller-free + */ static char * replace_python_install_mirror (const char *content, const char *url) { return replace_key_value (content, "python-install-mirror", url, NULL); } +/** + * @brief uv.toml 便捷包装: 提取顶层键的字符串值。 + * + * @param content 文件内容 + * @param key 键名 + * + * @return malloc 的 caller-free 解码后字符串; 未找到返回 NULL + * + * @memory SAFE + * return caller-free + */ static char * uvh_get_top_level_value (const char *content, const char *key) { diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c index 3eef96af..dc316f5a 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv.c @@ -43,17 +43,82 @@ pl_python_uv_prelude (void) * uv 配置文件选择顺序 (chsrc 写入策略): * 1. ./uv.toml (项目级;与 pyproject.toml 并存时 uv 优先读取它) * 2. ./pyproject.toml (项目级 [tool.uv] 段) - * 3. 用户级 uv.toml + * 3. 用户级 uv.toml(Unix 下遵循 XDG_CONFIG_HOME) * * uv 还会向父目录查找最近的项目配置,并合并用户/系统配置;chsrc 为避免 * 越界修改父项目,只管理当前目录或明确的用户级配置文件。 + * Unix 用户配置目录优先使用 $XDG_CONFIG_HOME/uv,未设置时回退到 ~/.config/uv。 */ #define PL_Python_uv_ConfigFile "uv.toml" #define PL_Python_uv_PyprojectConfigFile "pyproject.toml" /* 注意: 不能以 "./" 开头 —— Windows 上 chsrc_backup 的 copy 命令无法处理 */ #define PL_Python_uv_Local_ConfigPath "" -#define PL_Python_uv_User_ConfigPath "~/.config/uv/" +#define PL_Python_uv_User_ConfigPath "~/.config" + +static const char * +pl_python_uv_user_config_path (void) +{ + const char *xdg = getenv ("XDG_CONFIG_HOME"); + return (xdg && *xdg) ? xdg : PL_Python_uv_User_ConfigPath; +} + +static Source_t +pl_python_uv_yield_target_source (Target_t *target, char *option) +{ + if (!target->inited) target->preludefn (); + + if (hp_is_url (option)) + { + Source_t user = { &UserDefinedProvider, option }; + return user; + } + + int index = use_specific_mirror_or_auto_select (option, target); + return target->sources[index]; +} + +static char * +pl_python_uv_read_config (const char *path) +{ + if (!xy_file_exist (path)) return xy_strdup (""); + + char *normalized = xy_normalize_path (path); + FILE *fp = fopen (normalized, "rb"); + free (normalized); + if (!fp || fseek (fp, 0, SEEK_END) != 0) + { + if (fp) fclose (fp); + chsrc_error2 ("无法读取 uv 配置文件"); + return NULL; + } + + long size = ftell (fp); + if (size < 0) + { + fclose (fp); + chsrc_error2 ("无法读取 uv 配置文件"); + return NULL; + } + rewind (fp); + + char *raw = xy_malloc0 ((size_t)size + 1); + size_t nread = fread (raw, 1, (size_t)size, fp); + bool failed = ferror (fp) != 0; + fclose (fp); + if (failed) + { + free (raw); + chsrc_error2 ("无法读取 uv 配置文件"); + return NULL; + } + raw[nread] = '\0'; + + char *content = xy_str_gsub (raw, "\r\n", "\n"); + free (raw); + xy_ptr_replace (&content, xy_str_gsub (content, "\r", "\n")); + return content; +} char * @@ -86,23 +151,27 @@ pl_python_find_uv_config (bool mkdir) return NULL; } - char *config_dir = xy_2strcat(appdata, "\\uv\\"); + char *config_dir = xy_path_join (appdata, "uv"); if (mkdir) { chsrc_ensure_dir (config_dir); } - char *result = xy_2strcat (config_dir, PL_Python_uv_ConfigFile); + char *result = xy_path_join (config_dir, PL_Python_uv_ConfigFile); free (config_dir); return result; } else { /* config path on Linux or macOS */ + const char *config_path = pl_python_uv_user_config_path (); + char *config_dir = xy_path_join (config_path, "uv"); if (mkdir) { - chsrc_ensure_dir (PL_Python_uv_User_ConfigPath); + chsrc_ensure_dir (config_dir); } - return xy_2strcat (PL_Python_uv_User_ConfigPath, PL_Python_uv_ConfigFile); + char *result = xy_path_join (config_dir, PL_Python_uv_ConfigFile); + free (config_dir); + return result; } } } @@ -124,7 +193,7 @@ pl_python_uv_getsrc (char *option) } /* uv.toml 与 pyproject.toml 均使用同一套受限 TOML 读取逻辑。 */ - char *content = xy_file_read (uv_config); + char *content = pl_python_uv_read_config (uv_config); if (!content) { chsrc_error2 ("无法读取 uv 配置文件"); @@ -169,7 +238,7 @@ pl_python_uv_getsrc (char *option) /* * Python下载镜像 (python-install-mirror) * - * 内部 target, 不注册到 menu, 仅用于 chsrc_yield_source 自动测速选取。 + * 内部 target, 不注册到 menu, 仅用于本 recipe 独立选择 Python 下载镜像。 * 针对 python-build-standalone 的测速链接。 */ @@ -193,8 +262,8 @@ pl_uv_github_release_prelude (void) { chef_prep_this (pl_uv_github_release, gsr); - chef_set_recipe_created_on (this, "2026-05-31"); - chef_set_recipe_last_updated (this, "2026-05-31"); + chef_set_recipe_created_on (this, "2026-08-02"); + chef_set_recipe_last_updated (this, "2026-08-02"); chef_set_chefs (this, 1, "@Mikachu2333"); chef_set_sauciers (this, 0); @@ -230,8 +299,8 @@ pl_uv_github_release_prelude (void) static void pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char *py_dl_url) { - char *content = xy_file_read (uv_config); - if (!content) content = xy_strdup (""); + char *content = pl_python_uv_read_config (uv_config); + if (!content) return; char *final = NULL; if (xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile)) @@ -259,7 +328,7 @@ pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char * chsrc set uv * * 同时更换两部分: - * 1. PyPI 索引源 ([[index]] 表) + * 1. PyPI 索引源 ([[index]] 表) * 2. Python 解释器下载源 (python-install-mirror) * * @consult https://docs.astral.sh/uv/reference/settings/#python-install-mirror @@ -276,19 +345,21 @@ pl_python_uv_setsrc (char *option) return; } - /* reset: 把 index 与 python-install-mirror 写回默认上游 (PyPI 与 - * python-build-standalone), 不依赖 .bak 恢复旧 URL。 */ + /** + * reset: 把 index 与 python-install-mirror 写回 + * 默认上游(PyPI 与 python-build-standalone), 不依赖 .bak。 + */ if (chsrc_in_reset_mode ()) { if (!chsrc_check_file (uv_config)) { - chsrc_info ("没有 uv 配置文件, 无需重置"); + chsrc_info ("没有 uv 配置文件,无需重置"); free (uv_config); return; } - Source_t default_pypi = chsrc_yield_source (&pl_python_group_target, "upstream"); - Source_t default_gh = chsrc_yield_source (&pl_uv_github_release_target, "upstream"); + Source_t default_pypi = pl_python_uv_yield_target_source (&pl_python_group_target, "upstream"); + Source_t default_gh = pl_python_uv_yield_target_source (&pl_uv_github_release_target, "upstream"); chsrc_backup (uv_config); pl_python_uv_write_all (uv_config, default_pypi.url, default_gh.url); @@ -297,27 +368,39 @@ pl_python_uv_setsrc (char *option) return; } - /* set: 选取源并写入 */ + /** + * set: 选取源并写入。 + * 两个 URL 的语义不同:自定义 URL 只作为 PyPI index,Python 下载镜像仍自动测速。 */ char *pypi_opt = option; - char *gh_opt = option; + char *gh_opt = NULL; - /* option 命中内部 gh 源 (如 cnb) 时: gh 直接用指定源, pypi 自动测速 */ - if (gh_opt && !chsrc_in_reset_mode () && !hp_is_url (gh_opt)) + if (option && !hp_is_url (option)) { - bool found = false; if (!pl_uv_github_release_target.inited) pl_uv_github_release_target.preludefn (); + bool gh_found = false; + bool pypi_found = false; for (int i = 0; i < pl_uv_github_release_target.sources_n; i++) - if (xy_streql (pl_uv_github_release_target.sources[i].mirror->code, gh_opt)) - { found = true; break; } - if (found) - pypi_opt = NULL; /* cnb 等是 gh 专用源, pypi 部分自动测速 */ - else - gh_opt = NULL; + if (xy_streql (pl_uv_github_release_target.sources[i].mirror->code, option)) + { gh_found = true; break; } + if (!pl_python_group_target.inited) + pl_python_group_target.preludefn (); + for (int i = 0; i < pl_python_group_target.sources_n; i++) + if (xy_streql (pl_python_group_target.sources[i].mirror->code, option)) + { pypi_found = true; break; } + + if (gh_found && !pypi_found) + { + pypi_opt = NULL; + gh_opt = option; + } + else if (gh_found) + gh_opt = option; /* 共有 code: 两个 target 都使用同一 code */ } Source_t source = chsrc_yield_source (&pl_python_group_target, pypi_opt); - Source_t gh_source = chsrc_yield_source (&pl_uv_github_release_target, gh_opt); + /* 内部 target 不得复用 Python group leader 的数组下标。 */ + Source_t gh_source = pl_python_uv_yield_target_source (&pl_uv_github_release_target, gh_opt); if (chsrc_in_standalone_mode()) chsrc_confirm_source (&source); diff --git a/test/uv-helper-test.c b/test/uv-helper-test.c index eb6da5d3..192643dd 100644 --- a/test/uv-helper-test.c +++ b/test/uv-helper-test.c @@ -329,6 +329,46 @@ test_uvh_get_value_pyproject (void) } +static void +test_uvh_edge_cases (void) +{ + char *r; + + r = replace_pypi_index_url ("", "https://example.test/a\\b\"c"); + CHECK_STR (r, "[[index]]\nurl = \"https://example.test/a\\\\b\\\"c\"\ndefault = true\n"); + free (r); + + r = replace_pypi_index_url ( + "[[index]]\n\"url\" = \"https://old\"\ndefault = true\n", + "https://new"); + CHECK_STR (r, "[[index]]\nurl = \"https://new\"\ndefault = true\n"); + free (r); + + r = replace_key_value ( + "[tool.\"uv\"]\npython-install-mirror = \"https://old\"\n", + "python-install-mirror", "https://new", "[tool.uv]"); + CHECK_STR (r, "[tool.\"uv\"]\npython-install-mirror = \"https://new\"\n"); + free (r); + + r = replace_key_value ( + "[tool.'uv']\n'python-install-mirror' = \"https://old\"\n", + "python-install-mirror", "https://new", "[tool.uv]"); + CHECK_STR (r, "[tool.'uv']\npython-install-mirror = \"https://new\"\n"); + free (r); + + char *v = uvh_get_index_url ( + "[[index]]\nurl = \"https://example/\\u0073imple/\\U0001F600\"\ndefault = true\n", + "[[index]]", NULL); + CHECK (v != NULL); + if (v) { CHECK_STR (v, "https://example/simple/\xF0\x9F\x98\x80"); free (v); } + + r = replace_pypi_index_url ( + "[[index]]\nurl = \"https://old\"", "https://new"); + CHECK_STR (r, "[[index]]\nurl = \"https://new\""); + free (r); +} + + int main (void) { @@ -338,6 +378,7 @@ main (void) test_replace_index_url_pyproject (); test_replace_key_value_pyproject (); test_uvh_get_value_pyproject (); + test_uvh_edge_cases (); if (failures) { From fc45c524a6e5188b832309df884c27c11a947b0e Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 23:01:03 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96python?= =?UTF-8?q?=E9=95=9C=E5=83=8F=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/recipe/lang/Python/common.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/recipe/lang/Python/common.h b/src/recipe/lang/Python/common.h index a142a557..ecb7e3ba 100644 --- a/src/recipe/lang/Python/common.h +++ b/src/recipe/lang/Python/common.h @@ -60,16 +60,16 @@ pl_python_group_prelude (void) // @ref https://mirrors.jlu.edu.cn/_news/#2025-04-06-pypi-repo-down // {&Jlu, "https://mirrors.jlu.edu.cn/pypi/web/simple", FeedByPrelude}, {&Sjtug_Siyuan, "https://mirror.sjtu.edu.cn/pypi/web/simple", FeedByPrelude}, - {&Tuna, "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",FeedByPrelude}, + {&Tuna, "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple", FeedByPrelude}, {&Ustc, "https://mirrors.ustc.edu.cn/pypi/simple", FeedByPrelude}, {&Ali, "https://mirrors.aliyun.com/pypi/simple", FeedByPrelude}, {&Nju, "https://mirror.nju.edu.cn/pypi/web/simple", FeedByPrelude}, {&Pku, "https://mirrors.pku.edu.cn/pypi/web/simple", FeedByPrelude}, {&Tencent, "https://mirrors.cloud.tencent.com/pypi/simple", FeedByPrelude}, - // {&Tencent_Intra, "https://mirrors.cloud.tencentyun.com/pypi/simple",FeedByPrelude} - {&Huawei, "https://mirrors.huaweicloud.com/repository/pypi/simple",FeedByPrelude}, - {&Hust, "https://mirrors.hust.edu.cn/pypi/web/simple",FeedByPrelude} + // {&Tencent_Intra, "https://mirrors.cloud.tencentyun.com/pypi/simple", FeedByPrelude} + {&Huawei, "https://mirrors.huaweicloud.com/repository/pypi/simple", FeedByPrelude}, + {&Hust, "https://mirrors.hust.edu.cn/pypi/web/simple", FeedByPrelude} /* 不启用原因:24小时更新一次 */ // {&Netease, "https://mirrors.163.com/.help/pypi.html", NULL} From ff1a28163f1f22943463d29812ad57c1e9c7e1e0 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Sun, 2 Aug 2026 23:53:27 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dcopilot=E6=8F=90?= =?UTF-8?q?=E5=88=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- justfile | 5 +++-- src/recipe/lang/Python/uv.c | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/justfile b/justfile index 7eb677d5..032f1c9c 100644 --- a/justfile +++ b/justfile @@ -29,7 +29,8 @@ # 而不清楚 just 在 MINGW32 中的情况,所以我们在此 justfile 中并不实现关于 CI 的功能 # -------------------------------------------------------------- -set windows-shell := ['cmd', '/c'] +[windows] +set shell := ['cmd', '/c'] CC := if os() == 'windows' { "gcc" @@ -145,7 +146,7 @@ test-fw: @{{BIN_fw}} test-uv-helper: - @{{CC}} test/uv-helper-test.c -o uv-helper-test + @{{CC}} test/uv-helper-test.c {{CFLAGS_debug_mode}} -o uv-helper-test @{{BIN_uv-helper-test}} fastcheck: diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c index dc316f5a..a36aafcc 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv.c @@ -114,10 +114,7 @@ pl_python_uv_read_config (const char *path) } raw[nread] = '\0'; - char *content = xy_str_gsub (raw, "\r\n", "\n"); - free (raw); - xy_ptr_replace (&content, xy_str_gsub (content, "\r", "\n")); - return content; + return raw; } From 033939e9785ddbf921e326303d3957e6d977cd1d Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Mon, 3 Aug 2026 07:56:35 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20uv-helper-test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 7 +- justfile | 8 +- test/uv-helper-test.c | 391 ------------------------------------------ 3 files changed, 2 insertions(+), 404 deletions(-) delete mode 100644 test/uv-helper-test.c diff --git a/Makefile b/Makefile index 9915410f..6943fbea 100755 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ build-in-ci-release-mode: debug: build-in-debug-mode @$(DEBUGGER) $(DebugMode-Target-Name) -test: test-make-env test-xy test-fw test-uv-helper +test: test-make-env test-xy test-fw test-make-env: @echo "On-Linux: $(On-Linux)" @@ -193,10 +193,6 @@ test-fw: @$(CC) test/fw.c $(CFLAGS) -o fw @./fw -test-uv-helper: - @$(CC) test/uv-helper-test.c $(CFLAGS) -o uv-helper-test - @./uv-helper-test - check: test # AUR package 安装时将执行此 target @@ -211,7 +207,6 @@ clean: -@rm *.res 2>/dev/null -@rm xy 2>/dev/null -@rm fw 2>/dev/null - -@rm uv-helper-test 2>/dev/null -@rm README.md.bak* 2>/dev/null -@rm chsrc 2>/dev/null diff --git a/justfile b/justfile index 032f1c9c..2554a543 100644 --- a/justfile +++ b/justfile @@ -94,7 +94,6 @@ CFLAGS_release_mode := CFLAGS_release_mode_prompt + ' ' + CFLAGS_warn BIN_xy := if os() == 'windows' {'xy.exe'} else {'./xy'} BIN_fw := if os() == 'windows' {'fw.exe'} else {'./fw'} -BIN_uv-helper-test := if os() == 'windows' {'uv-helper-test.exe'} else {'./uv-helper-test'} BIN_rm := if os() == 'windows' {'del'} else {'rm'} #======================= alias b := build-in-dev-mode @@ -134,7 +133,7 @@ build-in-release-mode: debug: build-in-debug-mode @{{DEBUGGER}} {{DebugMode-Target-Name}} -test: test-xy test-fw test-uv-helper +test: test-xy test-fw # 这两个测试文件都用 DEBUG mode test-xy: @@ -145,10 +144,6 @@ test-fw: @{{CC}} test/fw.c {{CFLAGS_debug_mode}} -o fw @{{BIN_fw}} -test-uv-helper: - @{{CC}} test/uv-helper-test.c {{CFLAGS_debug_mode}} -o uv-helper-test - @{{BIN_uv-helper-test}} - fastcheck: @perl ./test/cli.pl fastcheck @@ -160,7 +155,6 @@ clean: -@{{BIN_rm}} *.res -@{{BIN_rm}} xy -@{{BIN_rm}} fw - -@{{BIN_rm}} uv-helper-test -@{{BIN_rm}} chsrc -@{{BIN_rm}} chsrc-debug -@{{BIN_rm}} chsrc-release diff --git a/test/uv-helper-test.c b/test/uv-helper-test.c deleted file mode 100644 index 192643dd..00000000 --- a/test/uv-helper-test.c +++ /dev/null @@ -1,391 +0,0 @@ -/** ------------------------------------------------------------ - * SPDX-License-Identifier: GPL-3.0-or-later - * ------------------------------------------------------------- - * uv-helper.c (uv.toml 字符串改写工具) 单元测试 - * - * 独立编译: 不依赖 xy.h / framework, 直接 #include uv-helper.c - * ------------------------------------------------------------*/ - -#include -#include -#include - -#include "../src/recipe/lang/Python/uv-helper.c" - -static int total = 0; -static int failures = 0; - -#define CHECK(cond) \ - do { \ - total++; \ - if (!(cond)) \ - { \ - failures++; \ - printf ("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ - } \ - } while (0) - -#define CHECK_STR(got, expect) \ - do { \ - total++; \ - if (strcmp ((got), (expect)) != 0) \ - { \ - failures++; \ - printf ("FAIL %s:%d:\n expected [%s]\n got [%s]\n", \ - __FILE__, __LINE__, (expect), (got)); \ - } \ - } while (0) - - -static void -test_replace_pypi_index_url (void) -{ - char *r; - - /* 空文件: 追加整个段 */ - r = replace_pypi_index_url ("", "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 已有 [[index]] 与 url: 仅替换 url 行 */ - r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://old\"\ndefault = true\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* default = false 保留 */ - r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://old\"\ndefault = false\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = false\n"); - free (r); - - /* [[index]] 段内没有 url 键: 在段内第一行之前插入 url + default */ - r = replace_pypi_index_url ( - "[[index]]\nname = \"tuna\"\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\ndefault = true\nname = \"tuna\"\n"); - free (r); - - /* 注释中的 [[index]] 不应被当作真实段: 追加新段 */ - r = replace_pypi_index_url ( - "# [[index]]\nfoo = 1\n", - "https://mirror"); - CHECK_STR (r, "# [[index]]\nfoo = 1\n[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 注释与真实段并存: 修改真实段 */ - r = replace_pypi_index_url ( - "# [[index]]\n[[index]]\nurl = \"https://old\"\n", - "https://mirror"); - CHECK_STR (r, "# [[index]]\n[[index]]\nurl = \"https://mirror\"\n"); - free (r); - - /* 单引号字符串值也能被替换 (替换后统一为双引号) */ - r = replace_pypi_index_url ( - "[[index]]\nurl = 'https://old'\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n"); - free (r); - - /* 多个 [[index]] 段: 优先修改 default = true 的 PyPI 替代源 */ - r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://private\"\ndefault = false\n\n[[index]]\nurl = \"https://old\"\ndefault = true\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://private\"\ndefault = false\n\n[[index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 段边界: [[index]] 之后其他表的 url 键不被误改 */ - r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://old\"\n\n[python]\nurl = \"https://python\"\n", - "https://mirror"); - CHECK_STR (r, "[[index]]\nurl = \"https://mirror\"\n\n[python]\nurl = \"https://python\"\n"); - free (r); -} - - -static void -test_replace_python_install_mirror (void) -{ - char *r; - - /* 空文件 */ - r = replace_python_install_mirror ("", "https://mirror"); - CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n"); - free (r); - - /* 顶层已有: 就地替换 */ - r = replace_python_install_mirror ( - "python-install-mirror = \"https://old\"\nother = 1\n", - "https://mirror"); - CHECK_STR (r, "python-install-mirror = \"https://mirror\"\nother = 1\n"); - free (r); - - /* 顶层键在段头之前: 替换后仍是顶层键 */ - r = replace_python_install_mirror ( - "python-install-mirror = \"https://old\"\n\n[python]\nx = 1\n", - "https://mirror"); - CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n\n[python]\nx = 1\n"); - free (r); - - /* 顶层无旧键但有段: 在第一个段头之前插入, 保证新键仍属顶层 */ - r = replace_python_install_mirror ( - "[python]\nx = 1\n", - "https://mirror"); - CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n[python]\nx = 1\n"); - free (r); - - /* 段内同名键不受影响 (chsrc 只管理顶层键) */ - r = replace_python_install_mirror ( - "[python]\npython-install-mirror = \"https://old\"\n", - "https://mirror"); - CHECK_STR (r, "python-install-mirror = \"https://mirror\"\n[python]\npython-install-mirror = \"https://old\"\n"); - free (r); - - /* 注释行不误删 */ - r = replace_python_install_mirror ( - "# python-install-mirror = \"comment\"\npython-install-mirror = \"https://old\"\n", - "https://mirror"); - CHECK_STR (r, "# python-install-mirror = \"comment\"\npython-install-mirror = \"https://mirror\"\n"); - free (r); -} - - -static void -test_uvh_get_top_level_value (void) -{ - char *v; - - v = uvh_get_top_level_value ( - "python-install-mirror = \"https://x\"\n", "python-install-mirror"); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://x"); free (v); } - - /* 单引号 */ - v = uvh_get_top_level_value ( - "python-install-mirror = 'https://x'\n", "python-install-mirror"); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://x"); free (v); } - - /* 等号周围无空格 */ - v = uvh_get_top_level_value ( - "python-install-mirror=\"https://x\"\n", "python-install-mirror"); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://x"); free (v); } - - /* 键位于段内 (非顶层) 时返回 NULL */ - v = uvh_get_top_level_value ( - "[python]\npython-install-mirror = \"https://x\"\n", "python-install-mirror"); - CHECK (v == NULL); - - /* 键不存在时返回 NULL */ - v = uvh_get_top_level_value ("other = 1\n", "python-install-mirror"); - CHECK (v == NULL); - - /* 注释行中的键不匹配 */ - v = uvh_get_top_level_value ( - "# python-install-mirror = \"comment\"\n", "python-install-mirror"); - CHECK (v == NULL); -} - - -static void -test_replace_index_url_pyproject (void) -{ - char *r; - - /* 空文件: 创建 [tool.uv] 表 + [[tool.uv.index]] 段 */ - r = replace_index_url ("", "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 已有 [tool.uv] 无 index 段: 表内追加 (在下一个段头前) */ - r = replace_index_url ( - "[tool.uv]\nsome = 1\n[tool.poetry]\nx = 1\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\nsome = 1\n[tool.poetry]\nx = 1\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 已有 [tool.uv] + [[tool.uv.index]]: 替换 url */ - r = replace_index_url ( - "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://old\"\ndefault = true\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); - - /* 有 [project] 等其他表时正确找到 [tool.uv] */ - r = replace_index_url ( - "[project]\nname = \"x\"\n\n[tool.uv]\n[[tool.uv.index]]\nurl = \"https://old\"\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, "[project]\nname = \"x\"\n\n[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\n"); - free (r); - - /* [[tool.uv.index]] 段内无 url 键: 段内插入 url + default */ - r = replace_index_url ( - "[tool.uv]\n[[tool.uv.index]]\nname = \"mirror\"\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\nname = \"mirror\"\n"); - free (r); - - /* CRLF 表头与缩进键应被识别,不能产生 duplicate table/key */ - r = replace_index_url ( - " [tool.uv]\r\n python-install-mirror = \"https://old-python\"\r\n [[tool.uv.index]]\r\n url = \"https://old\"\r\n default = true\r\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, - " [tool.uv]\r\n python-install-mirror = \"https://old-python\"\r\n [[tool.uv.index]]\r\n url = \"https://mirror\"\r\n default = true\r\n"); - free (r); - - /* CRLF 文件中无 [tool.uv]: 新建段同样使用 CRLF */ - r = replace_index_url ( - "[project]\r\nname = \"x\"\r\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, - "[project]\r\nname = \"x\"\r\n\r\n[tool.uv]\r\n[[tool.uv.index]]\r\nurl = \"https://mirror\"\r\ndefault = true\r\n"); - free (r); - - /* [tool.uv.sources] 不会阻止后续 index 被找到 */ - r = replace_index_url ( - "[tool.uv]\n[tool.uv.sources]\nfoo = { path = \"foo\" }\n[[tool.uv.index]]\nurl = \"https://old\"\ndefault = true\n", - "https://mirror", "[[tool.uv.index]]", "[tool.uv]"); - CHECK_STR (r, - "[tool.uv]\n[tool.uv.sources]\nfoo = { path = \"foo\" }\n[[tool.uv.index]]\nurl = \"https://mirror\"\ndefault = true\n"); - free (r); -} - - -static void -test_replace_key_value_pyproject (void) -{ - char *r; - - /* 空文件: 创建 [tool.uv] 表 + 键 */ - r = replace_key_value ("", "python-install-mirror", "https://mirror", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n"); - free (r); - - /* [tool.uv] 内已有键: 替换 */ - r = replace_key_value ( - "[tool.uv]\npython-install-mirror = \"https://old\"\n", - "python-install-mirror", "https://mirror", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n"); - free (r); - - /* [tool.uv] 内无该键但已有 [[tool.uv.index]] 子表: 键插在子表头前 */ - r = replace_key_value ( - "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://pypi\"\n", - "python-install-mirror", "https://mirror", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\npython-install-mirror = \"https://mirror\"\n[[tool.uv.index]]\nurl = \"https://pypi\"\n"); - free (r); - - /* 缩进键就地替换,不重复定义 */ - r = replace_key_value ( - "[tool.uv]\r\n python-install-mirror = \"https://old\"\r\n", - "python-install-mirror", "https://mirror", "[tool.uv]"); - CHECK_STR (r, "[tool.uv]\r\n python-install-mirror = \"https://mirror\"\r\n"); - free (r); - - /* CRLF 文件中无 [tool.uv]: 新建表同样使用 CRLF */ - r = replace_key_value ( - "[project]\r\nname = \"x\"\r\n", - "python-install-mirror", "https://mirror", "[tool.uv]"); - CHECK_STR (r, - "[project]\r\nname = \"x\"\r\n\r\n[tool.uv]\r\npython-install-mirror = \"https://mirror\"\r\n"); - free (r); -} - - -static void -test_uvh_get_value_pyproject (void) -{ - char *v; - - /* 表内键提取 */ - v = uvh_get_value_in_table ( - "[tool.uv]\npython-install-mirror = \"https://x\"\n", "python-install-mirror", "[tool.uv]"); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://x"); free (v); } - - /* 其他表内同名键: 找不到 */ - v = uvh_get_value_in_table ( - "[tool.other]\npython-install-mirror = \"https://x\"\n", "python-install-mirror", "[tool.uv]"); - CHECK (v == NULL); - - /* index url 提取 */ - v = uvh_get_index_url ( - "[tool.uv]\n[[tool.uv.index]]\nurl = \"https://pypi\"\ndefault = true\n", - "[[tool.uv.index]]", "[tool.uv]"); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://pypi"); free (v); } - - /* 无 index 段: NULL */ - v = uvh_get_index_url ( - "[tool.uv]\nsome = 1\n", "[[tool.uv.index]]", "[tool.uv]"); - CHECK (v == NULL); - - /* 表不存在: NULL */ - v = uvh_get_index_url ("", "[[tool.uv.index]]", "[tool.uv]"); - CHECK (v == NULL); -} - - -static void -test_uvh_edge_cases (void) -{ - char *r; - - r = replace_pypi_index_url ("", "https://example.test/a\\b\"c"); - CHECK_STR (r, "[[index]]\nurl = \"https://example.test/a\\\\b\\\"c\"\ndefault = true\n"); - free (r); - - r = replace_pypi_index_url ( - "[[index]]\n\"url\" = \"https://old\"\ndefault = true\n", - "https://new"); - CHECK_STR (r, "[[index]]\nurl = \"https://new\"\ndefault = true\n"); - free (r); - - r = replace_key_value ( - "[tool.\"uv\"]\npython-install-mirror = \"https://old\"\n", - "python-install-mirror", "https://new", "[tool.uv]"); - CHECK_STR (r, "[tool.\"uv\"]\npython-install-mirror = \"https://new\"\n"); - free (r); - - r = replace_key_value ( - "[tool.'uv']\n'python-install-mirror' = \"https://old\"\n", - "python-install-mirror", "https://new", "[tool.uv]"); - CHECK_STR (r, "[tool.'uv']\npython-install-mirror = \"https://new\"\n"); - free (r); - - char *v = uvh_get_index_url ( - "[[index]]\nurl = \"https://example/\\u0073imple/\\U0001F600\"\ndefault = true\n", - "[[index]]", NULL); - CHECK (v != NULL); - if (v) { CHECK_STR (v, "https://example/simple/\xF0\x9F\x98\x80"); free (v); } - - r = replace_pypi_index_url ( - "[[index]]\nurl = \"https://old\"", "https://new"); - CHECK_STR (r, "[[index]]\nurl = \"https://new\""); - free (r); -} - - -int -main (void) -{ - test_replace_pypi_index_url (); - test_replace_python_install_mirror (); - test_uvh_get_top_level_value (); - test_replace_index_url_pyproject (); - test_replace_key_value_pyproject (); - test_uvh_get_value_pyproject (); - test_uvh_edge_cases (); - - if (failures) - { - printf ("uv-helper-test: %d/%d checks FAILED\n", failures, total); - return 1; - } - - printf ("uv-helper-test: all %d checks passed\n", total); - return 0; -} From 4f89e3fbff43088000880a2629f2bde504311aac Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Mon, 3 Aug 2026 08:20:25 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=20uv=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=88=B0=E7=8B=AC=E7=AB=8B=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/recipe/lang/Python/{ => uv}/uv-helper.c | 0 src/recipe/lang/Python/{ => uv}/uv.c | 46 +++++++++++---------- src/recipe/menu.c | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) rename src/recipe/lang/Python/{ => uv}/uv-helper.c (100%) rename src/recipe/lang/Python/{ => uv}/uv.c (87%) diff --git a/src/recipe/lang/Python/uv-helper.c b/src/recipe/lang/Python/uv/uv-helper.c similarity index 100% rename from src/recipe/lang/Python/uv-helper.c rename to src/recipe/lang/Python/uv/uv-helper.c diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv/uv.c similarity index 87% rename from src/recipe/lang/Python/uv.c rename to src/recipe/lang/Python/uv/uv.c index a36aafcc..b731ebce 100644 --- a/src/recipe/lang/Python/uv.c +++ b/src/recipe/lang/Python/uv/uv.c @@ -6,10 +6,11 @@ def_target(pl_python_uv, "uv"); #include "uv-helper.c" -/* 内部 target 前置声明 (定义在文件末尾 def_target 处) */ +// 内部 target 前置声明 (定义在文件末尾 def_target 处) void pl_uv_github_release_prelude (void); extern Target_t pl_uv_github_release_target; +// 调整 `python-install-mirror` 相关配置应在 `pl_uv_github_release` 处修改,本处仅为 uv 最常用的 `pypi` 镜像配置。 void pl_python_uv_prelude (void) { @@ -31,7 +32,7 @@ pl_python_uv_prelude (void) chef_use_other_target_sources (this, &pl_python_group_target); - /* 内部 target 的 prelude 未通过 menu.c 的 add() 注册, 手动挂载 */ + // 内部 target 的 prelude 未通过 menu.c 的 add() 注册, 手动挂载 pl_uv_github_release_target.preludefn = pl_uv_github_release_prelude; } @@ -52,7 +53,7 @@ pl_python_uv_prelude (void) #define PL_Python_uv_ConfigFile "uv.toml" #define PL_Python_uv_PyprojectConfigFile "pyproject.toml" -/* 注意: 不能以 "./" 开头 —— Windows 上 chsrc_backup 的 copy 命令无法处理 */ +// 注意: 不能以 "./" 开头 —— Windows 上 chsrc_backup 的 copy 命令无法处理 #define PL_Python_uv_Local_ConfigPath "" #define PL_Python_uv_User_ConfigPath "~/.config" @@ -123,7 +124,7 @@ pl_python_find_uv_config (bool mkdir) { if (chsrc_in_project_scope_mode()) { - /* uv.toml 与 pyproject.toml 并存时,uv 会忽略后者中的 [tool.uv]。 */ + // uv.toml 与 pyproject.toml 并存时,uv 会忽略后者中的 [tool.uv]。 char *uv_toml = xy_2strcat (PL_Python_uv_Local_ConfigPath, PL_Python_uv_ConfigFile); if (xy_file_exist (uv_toml)) return uv_toml; @@ -139,7 +140,7 @@ pl_python_find_uv_config (bool mkdir) { if (xy.on_windows) { - /* config path on Windows */ + // config path on Windows char *appdata = getenv ("APPDATA"); if (!appdata) @@ -159,7 +160,7 @@ pl_python_find_uv_config (bool mkdir) } else { - /* config path on Linux or macOS */ + // config path on Linux or macOS const char *config_path = pl_python_uv_user_config_path (); char *config_dir = xy_path_join (config_path, "uv"); if (mkdir) @@ -189,7 +190,7 @@ pl_python_uv_getsrc (char *option) return; } - /* uv.toml 与 pyproject.toml 均使用同一套受限 TOML 读取逻辑。 */ + // uv.toml 与 pyproject.toml 均使用同一套受限 TOML 读取逻辑。 char *content = pl_python_uv_read_config (uv_config); if (!content) { @@ -241,18 +242,18 @@ pl_python_uv_getsrc (char *option) def_target (pl_uv_github_release, NULL); -/* CNB (Cloud Native Build, 腾讯) 托管的 python-build-standalone releases 镜像 */ +// CNB (Cloud Native Build, 腾讯) 托管的 python-build-standalone releases 镜像 static MirrorSite_t CnbPython = { IS_DedicatedMirrorSite, "cnb", "CNB", "Cloud Native Build (腾讯)", "https://cnb.cool/", - {NotSkip, NA, NA, "https://cnb.cool/astral-sh/python-build-standalone/-/releases/download/20260728/cpython-3.14.6+20260728-i686-pc-windows-msvc-install_only_stripped.tar.gz", ACCURATE} + {NotSkip, NA, NA, "https://cnb.cool/astral-sh/python-build-standalone/-/releases/download/20260728/cpython-3.15.0b4+20260728-x86_64_v4-unknown-linux-gnu-freethreaded-install_only.tar.gz", ACCURATE} }; -/* 内部 target, 无 CLI 入口, 以下为占位 */ -void pl_uv_github_release_getsrc (char *o) { (void)o; } -void pl_uv_github_release_setsrc (char *o) { (void)o; } -void pl_uv_github_release_resetsrc (char *o) { (void)o; } +// 内部 target, 无 CLI 入口, 以下为占位 +void pl_uv_github_release_getsrc (char *o) { (void) o; } +void pl_uv_github_release_setsrc (char *o) { (void) o; } +void pl_uv_github_release_resetsrc (char *o) { (void) o; } void pl_uv_github_release_prelude (void) @@ -282,10 +283,10 @@ pl_uv_github_release_prelude (void) chef_set_smURL_with_postfix (this, &CnbPython, GH_SM_POSTFIX); #undef GH_SM_POSTFIX - /* 2026-5-31: USTC 仅保留 Latest, 只能用 SHA256SUMS 粗略测速 */ + // 2026-5-31: USTC 仅保留 Latest, 只能用 SHA256SUMS 粗略测速 chef_set_smURL_with_postfix (this, &Ustc, "/LatestRelease/SHA256SUMS"); - /* 中科大仅保留 Latest 且文件内含动态版本号, 使用模糊测速 */ + // 中科大仅保留 Latest 且文件内含动态版本号, 使用模糊测速 chef_set_provider_sm_accuracy (&Ustc, ROUGH); } @@ -302,14 +303,14 @@ pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char char *final = NULL; if (xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile)) { - /* pyproject.toml: 配置位于 [tool.uv] 表内 */ + // `pyproject.toml`: 配置位于 `[tool.uv]` 表内 char *updated = replace_index_url (content, pypi_url, "[[tool.uv.index]]", "[tool.uv]"); final = replace_key_value (updated, "python-install-mirror", py_dl_url, "[tool.uv]"); free (updated); } else { - /* uv.toml: 配置位于顶层 */ + // `uv.toml`: 配置位于顶层 char *updated = replace_pypi_index_url (content, pypi_url); final = replace_python_install_mirror (updated, py_dl_url); free (updated); @@ -325,8 +326,8 @@ pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char * chsrc set uv * * 同时更换两部分: - * 1. PyPI 索引源 ([[index]] 表) - * 2. Python 解释器下载源 (python-install-mirror) + * 1. PyPI 索引源 (`[[index]]` 表) + * 2. Python 解释器下载源 (`python-install-mirror`) * * @consult https://docs.astral.sh/uv/reference/settings/#python-install-mirror */ @@ -367,7 +368,8 @@ pl_python_uv_setsrc (char *option) /** * set: 选取源并写入。 - * 两个 URL 的语义不同:自定义 URL 只作为 PyPI index,Python 下载镜像仍自动测速。 */ + * 两个 URL 的语义不同:自定义 URL 只作为 PyPI index,Python 下载镜像仍自动测速。 + */ char *pypi_opt = option; char *gh_opt = NULL; @@ -392,11 +394,11 @@ pl_python_uv_setsrc (char *option) gh_opt = option; } else if (gh_found) - gh_opt = option; /* 共有 code: 两个 target 都使用同一 code */ + gh_opt = option; // 共有 code: 两个 target 都使用同一 code } Source_t source = chsrc_yield_source (&pl_python_group_target, pypi_opt); - /* 内部 target 不得复用 Python group leader 的数组下标。 */ + // 内部 target 不得复用 Python group leader 的数组下标。 Source_t gh_source = pl_python_uv_yield_target_source (&pl_uv_github_release_target, gh_opt); if (chsrc_in_standalone_mode()) diff --git a/src/recipe/menu.c b/src/recipe/menu.c index a5037f55..5387e6f4 100644 --- a/src/recipe/menu.c +++ b/src/recipe/menu.c @@ -18,7 +18,7 @@ #include "lang/Python/Poetry.c" #include "lang/Python/PDM.c" #include "lang/Python/Rye.c" - #include "lang/Python/uv.c" + #include "lang/Python/uv/uv.c" #include "lang/Python/Python.c" #include "lang/JavaScript/common.h" From f7f721876b7908771f3473a3b7409c8c5a8c7b15 Mon Sep 17 00:00:00 2001 From: Aoran Zeng Date: Mon, 3 Aug 2026 09:30:08 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=20`uv`=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/lang/Python/uv/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename "doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" => src/recipe/lang/Python/uv/README.md (98%) diff --git "a/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" b/src/recipe/lang/Python/uv/README.md similarity index 98% rename from "doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" rename to src/recipe/lang/Python/uv/README.md index f938f57f..36660776 100644 --- "a/doc/12-uv\346\215\242\346\272\220\345\256\236\347\216\260\350\257\264\346\230\216.md" +++ b/src/recipe/lang/Python/uv/README.md @@ -2,7 +2,7 @@ ! SPDX-License-Identifier: GFDL-1.3-or-later ! ------------------------------------------------------------- ! Doc Type : Markdown - ! Doc Name : 12-uv换源实现说明.md + ! Doc Name : uv换源实现说明.md ! Created On : <2026-08-02> ! Last Modified : <2026-08-02> ! ---------------------------------------------------------- --> From bc3adab6d6673018d00a858739d51fe63c371a72 Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Mon, 3 Aug 2026 10:16:06 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" | 3 +-- lib/xy.h | 2 +- src/recipe/lang/Python/uv/README.md | 2 +- src/recipe/lang/Python/uv/uv-helper.c | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" index 5f3b7503..9cb54326 100644 --- "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" +++ "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" @@ -4,8 +4,7 @@ ! Doc Type : Markdown ! Doc Name : 10-如何编写recipe.md ! Doc Authors : 曾奥然 - ! Contributors : Nul None - Mikachu2333 + ! Contributors : Mikachu2333 ! | ! Created On : <2024-08-19> ! Last Modified : <2026-01-21> diff --git a/lib/xy.h b/lib/xy.h index 936edb92..21375659 100644 --- a/lib/xy.h +++ b/lib/xy.h @@ -762,7 +762,7 @@ xy_str_next_nonempty_line (const char *str) /** * @brief 读取文件内容并返回字符串,失败时返回空字符串 * - * @note 已处理 \r\n 和 \r,返回的字符串均为 \n 换行 + * @note 已处理 `\r\n` 和 `\r`,返回的字符串均为 `\n` 换行 * * @flavor Ruby: IO::read * diff --git a/src/recipe/lang/Python/uv/README.md b/src/recipe/lang/Python/uv/README.md index 36660776..406a5b54 100644 --- a/src/recipe/lang/Python/uv/README.md +++ b/src/recipe/lang/Python/uv/README.md @@ -65,7 +65,7 @@ ## 实现边界 -配置改写器只解析 chsrc 使用的 TOML 子集,但支持常见的行首缩进、CRLF、行尾注释、多个 index,以及位于 `[tool.uv.sources]` 等子表之后的 `[[tool.uv.index]]`。改写会保留原有缩进与行尾风格(CRLF/LF)。多行字符串和完整 TOML 转义语义不属于该轻量改写器的支持范围。 +配置改写器只解析 chsrc 使用的 TOML 子集,但支持常见的行首缩进、CRLF、行尾注释、多个 index,以及位于 `[tool.uv.sources]` 等子表之后的 `[[tool.uv.index]]`。读取时换行统一为 LF 处理,写入时按系统平台输出行尾(Windows 为 CRLF,其余为 LF)。多行字符串和完整 TOML 转义语义不属于该轻量改写器的支持范围。 参考: diff --git a/src/recipe/lang/Python/uv/uv-helper.c b/src/recipe/lang/Python/uv/uv-helper.c index 12f06abc..5d208a23 100644 --- a/src/recipe/lang/Python/uv/uv-helper.c +++ b/src/recipe/lang/Python/uv/uv-helper.c @@ -1,8 +1,8 @@ /** ------------------------------------------------------------ * SPDX-License-Identifier: GPL-3.0-or-later * ------------------------------------------------------------- - * Lib Authors : Mikachu2333 - * Contributors : EXAMPLE + * Lib Authors : Mikachu2333 + * Contributors : Nul None * | * Created On : <2026-08-02> * Last Modified : <2026-08-02> From 77ff0fa46b831207264a6b457cf5fb5acf869b4f Mon Sep 17 00:00:00 2001 From: Mikachu2333 Date: Mon, 3 Aug 2026 11:13:32 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E5=A4=8D=E7=94=A8xy=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=9A=84=E8=A7=A3=E6=9E=90=E5=87=BD=E6=95=B0=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96uv=E9=85=8D=E7=BD=AE=E5=86=99=E5=85=A5=E4=B8=8E?= =?UTF-8?q?=E6=8D=A2=E8=A1=8C=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/recipe/lang/Python/uv/uv-helper.c | 163 ++++---------------------- src/recipe/lang/Python/uv/uv.c | 86 ++++++-------- 2 files changed, 59 insertions(+), 190 deletions(-) diff --git a/src/recipe/lang/Python/uv/uv-helper.c b/src/recipe/lang/Python/uv/uv-helper.c index 5d208a23..2362b869 100644 --- a/src/recipe/lang/Python/uv/uv-helper.c +++ b/src/recipe/lang/Python/uv/uv-helper.c @@ -409,56 +409,11 @@ uvh_find_managed_index (const char *content, const char *index_header) return first; } -/** - * @brief 检测文件内容是否使用 CRLF 换行。 - * - * 以第一个换行符前是否有 '\r' 判断; 混合换行风格时结果取决于首个换行。 - * - * @param content 文件内容 - * - * @return 使用 CRLF 时返回 true, 否则返回 false - */ -static bool -uvh_detect_crlf (const char *content) -{ - const char *nl = strchr (content, '\n'); - return nl && nl > content && nl[-1] == '\r'; -} - -/** - * @brief 按 content 的换行风格转换 str 中的 LF。 - * - * content 为 CRLF 时返回 CRLF 版本, 否则原样返回。仅用于追加的纯 LF 文本, - * 不要传入已含 CRLF 的 content 本身。 - * - * @param content 用于判断换行风格的内容 - * @param str 纯 LF 的待转换文本 - * - * @return malloc 的 caller-free 转换后字符串 - * - * @memory SAFE - * return caller-free - */ -static char * -uvh_apply_eol (const char *content, const char *str) -{ - bool crlf = uvh_detect_crlf (content); - size_t n = strlen (str); - char *ret = malloc (n + (crlf ? n : 0) + 1); - size_t pos = 0; - for (const char *s = str; *s; s++) - { - if (crlf && *s == '\n') ret[pos++] = '\r'; - ret[pos++] = *s; - } - ret[pos] = '\0'; - return ret; -} - /** * @brief 用 new_line_text 替换 content 中从 line 开始的整行。 * - * 保留原行缩进、行尾风格 (CRLF/LF) 与后续内容; new_line_text 不含换行。 + * 保留原行缩进与后续内容; new_line_text 不含换行。中间改写统一使用 LF, + * 行尾由调用方在最终写盘前按系统平台转换 (Windows 为 CRLF, 其余 LF)。 * * @param content 原始内容 * @param line content 中待替换行的行首指针 @@ -475,9 +430,6 @@ uvh_replace_line (const char *content, const char *line, const char *new_line_te const char *line_end = strchr (line, '\n'); if (!line_end) line_end = content + strlen (content); - /* 原行为 CRLF 时,新行同样使用 CRLF */ - bool crlf = (*line_end == '\n' && line_end > line && line_end[-1] == '\r'); - /* 复用原行缩进 */ const char *indent_end = uvh_skip_indent (line); size_t ind = indent_end - line; @@ -488,7 +440,6 @@ uvh_replace_line (const char *content, const char *line, const char *new_line_te pos += snprintf (ret + pos, len - pos, "%.*s", (int)(line - content), content); pos += snprintf (ret + pos, len - pos, "%.*s", (int)ind, line); pos += snprintf (ret + pos, len - pos, "%s", new_line_text); - if (crlf) ret[pos++] = '\r'; strcpy (ret + pos, line_end); return ret; } @@ -496,8 +447,8 @@ uvh_replace_line (const char *content, const char *line, const char *new_line_te /** * @brief 在 insert_at 位置之前插入 insert_text (以 '\n' 结尾)。 * - * 内容为 CRLF 时, 插入文本与分隔换行同样转换为 CRLF; insert_at 之前 - * 没有换行时会先补一个分隔换行。 + * 中间改写统一使用 LF, 行尾由调用方在最终写盘前按系统平台转换; + * insert_at 之前没有换行时会先补一个分隔换行。 * * @param content 原始内容 * @param insert_at content 内的插入位置指针 @@ -514,29 +465,33 @@ uvh_insert_before (const char *content, const char *insert_at, const char *inser size_t head = insert_at - content; bool need_sep = (head > 0 && content[head - 1] != '\n'); - bool crlf = need_sep ? uvh_detect_crlf (content) - : (head >= 2 && content[head - 1] == '\n' && content[head - 2] == '\r'); size_t text_len = strlen (insert_text); - size_t extra = (crlf ? text_len : 0) + (need_sep ? 1 : 0); - size_t len = strlen (content) + text_len + extra + 16; + size_t len = strlen (content) + text_len + (need_sep ? 1 : 0) + 16; char *ret = calloc (len, 1); size_t pos = 0; pos += snprintf (ret + pos, len - pos, "%.*s", (int)head, content); if (need_sep) { - if (crlf) ret[pos++] = '\r'; ret[pos++] = '\n'; } - for (const char *t = insert_text; *t; t++) - { - if (crlf && *t == '\n') ret[pos++] = '\r'; - ret[pos++] = *t; - } + memcpy (ret + pos, insert_text, text_len); + pos += text_len; strcpy (ret + pos, content + head); return ret; } +/** + * @brief 以 LF 分隔,将完整 TOML 片段追加到文件末尾。 + * + * @return malloc 的 caller-free 新内容 + */ +static char * +uvh_append_segment (const char *content, const char *segment) +{ + return uvh_insert_before (content, content + strlen (content), segment); +} + /** * @brief 替换/创建 index 数组表 (如 [[index]] 或 [[tool.uv.index]]) 的 url 值。 @@ -567,23 +522,15 @@ replace_index_url (const char *content, const char *url, const char *index_heade if (!table) { /* 父表不存在: 在文件末尾创建父表并在其中创建 index 段, - * 换行风格与 content 保持一致 */ - size_t clen = strlen (content); - const char *sep = (clen > 0) ? "\n" : ""; + * 中间改写统一 LF, 行尾由最终写入前按系统平台转换 */ char *escaped_url = uvh_escape_basic_string (url); size_t seg_len = strlen (parent_table) + strlen (index_header) + strlen (escaped_url) + 48; char *seg = calloc (seg_len, 1); snprintf (seg, seg_len, "%s\n%s\nurl = \"%s\"\ndefault = true\n", parent_table, index_header, escaped_url); free (escaped_url); - char *seg_eol = uvh_apply_eol (content, seg); + char *ret = uvh_append_segment (content, seg); free (seg); - char *sep_eol = uvh_apply_eol (content, sep); - size_t len = clen + strlen (sep_eol) + strlen (seg_eol) + 1; - char *ret = calloc (len, 1); - snprintf (ret, len, "%s%s%s", content, sep_eol, seg_eol); - free (sep_eol); - free (seg_eol); return ret; } } @@ -663,22 +610,14 @@ replace_key_value (const char *content, const char *key, const char *url, const if (!table) { /* 父表不存在: 在文件末尾创建父表并写入键, - * 换行风格与 content 保持一致 */ - size_t clen = strlen (content); - const char *sep = (clen > 0) ? "\n" : ""; + * 中间改写统一 LF, 行尾由最终写入前按系统平台转换 */ char *escaped_url = uvh_escape_basic_string (url); size_t seg_len = strlen (parent_table) + strlen (key) + strlen (escaped_url) + 16; char *seg = calloc (seg_len, 1); snprintf (seg, seg_len, "%s\n%s = \"%s\"\n", parent_table, key, escaped_url); free (escaped_url); - char *seg_eol = uvh_apply_eol (content, seg); + char *ret = uvh_append_segment (content, seg); free (seg); - char *sep_eol = uvh_apply_eol (content, sep); - size_t len = clen + strlen (sep_eol) + strlen (seg_eol) + 1; - char *ret = calloc (len, 1); - snprintf (ret, len, "%s%s%s", content, sep_eol, seg_eol); - free (sep_eol); - free (seg_eol); return ret; } @@ -776,7 +715,6 @@ uvh_get_value_in_table (const char *content, const char *key, const char *parent * * @param content 文件内容 * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" - * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]" * * @return malloc 的 caller-free 解码后 URL; 未找到返回 NULL * @@ -784,10 +722,9 @@ uvh_get_value_in_table (const char *content, const char *key, const char *parent * return caller-free */ static char * -uvh_get_index_url (const char *content, const char *index_header, const char *parent_table) +uvh_get_index_url (const char *content, const char *index_header) { /* fully-qualified [[tool.uv.index]] 会隐式创建父路径,不要求显式 [tool.uv]。 */ - (void)parent_table; const char *ih = uvh_find_managed_index (content, index_header); if (!ih) return NULL; @@ -797,57 +734,3 @@ uvh_get_index_url (const char *content, const char *index_header, const char *pa if (!url_line) return NULL; return uvh_extract_string_value (url_line); } - - -/* ============ uv.toml 便捷包装 (保持原函数名) ============ */ - -/** - * @brief uv.toml 便捷包装: 替换/创建顶层 [[index]] 段的 url。 - * - * @param content 原始内容 - * @param url 新的索引 URL - * - * @return malloc 的 caller-free 新内容 - * - * @memory SAFE - * return caller-free - */ -static char * -replace_pypi_index_url (const char *content, const char *url) -{ - return replace_index_url (content, url, "[[index]]", NULL); -} - -/** - * @brief uv.toml 便捷包装: 替换/创建顶层 python-install-mirror 键。 - * - * @param content 原始内容 - * @param url 新的 Python 下载镜像基址 - * - * @return malloc 的 caller-free 新内容 - * - * @memory SAFE - * return caller-free - */ -static char * -replace_python_install_mirror (const char *content, const char *url) -{ - return replace_key_value (content, "python-install-mirror", url, NULL); -} - -/** - * @brief uv.toml 便捷包装: 提取顶层键的字符串值。 - * - * @param content 文件内容 - * @param key 键名 - * - * @return malloc 的 caller-free 解码后字符串; 未找到返回 NULL - * - * @memory SAFE - * return caller-free - */ -static char * -uvh_get_top_level_value (const char *content, const char *key) -{ - return uvh_get_value_in_table (content, key, NULL); -} diff --git a/src/recipe/lang/Python/uv/uv.c b/src/recipe/lang/Python/uv/uv.c index b731ebce..0da1c3aa 100644 --- a/src/recipe/lang/Python/uv/uv.c +++ b/src/recipe/lang/Python/uv/uv.c @@ -84,38 +84,19 @@ pl_python_uv_read_config (const char *path) { if (!xy_file_exist (path)) return xy_strdup (""); - char *normalized = xy_normalize_path (path); - FILE *fp = fopen (normalized, "rb"); - free (normalized); - if (!fp || fseek (fp, 0, SEEK_END) != 0) + char *file = xy_normalize_path (path); + FILE *f = fopen (file, "rb"); + if (!f) { - if (fp) fclose (fp); - chsrc_error2 ("无法读取 uv 配置文件"); - return NULL; - } - - long size = ftell (fp); - if (size < 0) - { - fclose (fp); - chsrc_error2 ("无法读取 uv 配置文件"); + free (file); return NULL; } - rewind (fp); + fclose (f); - char *raw = xy_malloc0 ((size_t)size + 1); - size_t nread = fread (raw, 1, (size_t)size, fp); - bool failed = ferror (fp) != 0; - fclose (fp); - if (failed) - { - free (raw); - chsrc_error2 ("无法读取 uv 配置文件"); - return NULL; - } - raw[nread] = '\0'; - - return raw; + /* xy_file_read 会把 CRLF 与孤立 CR 统一为 LF。 */ + char *content = xy_file_read (file); + free (file); + return content; } @@ -203,7 +184,7 @@ pl_python_uv_getsrc (char *option) const char *index_header = pyproject ? "[[tool.uv.index]]" : "[[index]]"; const char *parent_table = pyproject ? "[tool.uv]" : NULL; - char *url = uvh_get_index_url (content, index_header, parent_table); + char *url = uvh_get_index_url (content, index_header); if (url) { say (url); @@ -219,9 +200,7 @@ pl_python_uv_getsrc (char *option) say (default_source.url); } - char *mirror = pyproject - ? uvh_get_value_in_table (content, "python-install-mirror", parent_table) - : uvh_get_top_level_value (content, "python-install-mirror"); + char *mirror = uvh_get_value_in_table (content, "python-install-mirror", parent_table); if (mirror) { say (mirror); @@ -294,31 +273,30 @@ pl_uv_github_release_prelude (void) /** * 一次性完成uv配置文件的全部文件操作 (set 路径) */ -static void +static bool pl_python_uv_write_all (const char *uv_config, const char *pypi_url, const char *py_dl_url) { char *content = pl_python_uv_read_config (uv_config); - if (!content) return; - - char *final = NULL; - if (xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile)) - { - // `pyproject.toml`: 配置位于 `[tool.uv]` 表内 - char *updated = replace_index_url (content, pypi_url, "[[tool.uv.index]]", "[tool.uv]"); - final = replace_key_value (updated, "python-install-mirror", py_dl_url, "[tool.uv]"); - free (updated); - } - else + if (!content) { - // `uv.toml`: 配置位于顶层 - char *updated = replace_pypi_index_url (content, pypi_url); - final = replace_python_install_mirror (updated, py_dl_url); - free (updated); + chsrc_error2 ("无法读取 uv 配置文件"); + return false; } + bool pyproject = xy_str_end_with (uv_config, PL_Python_uv_PyprojectConfigFile); + const char *index_header = pyproject ? "[[tool.uv.index]]" : "[[index]]"; + const char *parent_table = pyproject ? "[tool.uv]" : NULL; + + char *updated = replace_index_url (content, pypi_url, index_header, parent_table); + char *final = replace_key_value (updated, "python-install-mirror", py_dl_url, parent_table); + free (updated); + + /* final 仍为纯 LF。框架最终以文本模式写入:Windows 写盘时转换为 CRLF, + * 其他平台保持 LF。 */ chsrc_overwrite_file (final, uv_config); free (final); free (content); + return true; } @@ -360,7 +338,11 @@ pl_python_uv_setsrc (char *option) Source_t default_gh = pl_python_uv_yield_target_source (&pl_uv_github_release_target, "upstream"); chsrc_backup (uv_config); - pl_python_uv_write_all (uv_config, default_pypi.url, default_gh.url); + if (!pl_python_uv_write_all (uv_config, default_pypi.url, default_gh.url)) + { + free (uv_config); + return; + } free (uv_config); return; @@ -405,7 +387,11 @@ pl_python_uv_setsrc (char *option) chsrc_confirm_source (&source); chsrc_backup (uv_config); - pl_python_uv_write_all (uv_config, source.url, gh_source.url); + if (!pl_python_uv_write_all (uv_config, source.url, gh_source.url)) + { + free (uv_config); + return; + } free (uv_config); if (chsrc_in_standalone_mode())