diff --git a/.github/workflows/PR-test.yml b/.github/workflows/PR-test.yml index 7ffa5bf88..9ec3ac045 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/.gitignore b/.gitignore index 2f9e20a29..668f68f01 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ *.out *.exe *.res +*.pdb ############################## diff --git a/.vscode/settings.json b/.vscode/settings.json index 893971313..6047583c0 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 } 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 a6633d819..9cb543269 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,8 @@ ! ------------------------------------------------------------- ! Doc Type : Markdown ! Doc Name : 10-如何编写recipe.md - ! Doc Authors : 曾奥然 - ! Contributors : Nul None + ! Doc Authors : 曾奥然 + ! Contributors : Mikachu2333 ! | ! Created On : <2024-08-19> ! Last Modified : <2026-01-21> diff --git a/justfile b/justfile index a7e4dc733..2554a5439 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" diff --git a/lib/xy.h b/lib/xy.h index 717445aad..21375659b 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 @@ -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/chsrc-main.c b/src/chsrc-main.c index 964b65af3..2b1ddc6ea 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/common.h b/src/recipe/lang/Python/common.h index a142a557a..ecb7e3ba1 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} diff --git a/src/recipe/lang/Python/uv.c b/src/recipe/lang/Python/uv.c deleted file mode 100644 index df41ee7fa..000000000 --- a/src/recipe/lang/Python/uv.c +++ /dev/null @@ -1,196 +0,0 @@ -/** ------------------------------------------------------------ - * SPDX-License-Identifier: GPL-3.0-or-later - * ------------------------------------------------------------*/ - -def_target(pl_python_uv, "uv"); - -void -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_chefs (this, 2, "@happy-game", "@MingriLingran"); - chef_set_sauciers (this, 2, "@Kattos", "@ccmywish"); - - chef_set_scope_cap (this, ProjectScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Able_But_Not_Implemented); - chef_set_default_scope (this, UserScope); - - chef_allow_english(this); - chef_allow_user_define(this); - - chef_use_other_target_sources (this, &pl_python_group_target); -} - - -/** - * chsrc get uv - * - * uv的配置优先级顺序如下(高到低): - * 1. $workspaces/uv.toml - * 2. $workspaces/pyproject.toml - * 3. ~/.config/uv/uv.toml - * 4. /etc/uv/uv.toml - */ - -#define PL_Python_uv_ConfigFile "uv.toml" -#define PL_Python_uv_Local_ConfigPath "./" -#define PL_Python_uv_User_ConfigPath "~/.config/uv/" - - -char * -pl_python_find_uv_config (bool mkdir) -{ - if (chsrc_in_project_scope_mode()) - { - return xy_2strcat (PL_Python_uv_Local_ConfigPath, PL_Python_uv_ConfigFile); - } - else - { - if (xy.on_windows) - { - /* config path on Windows */ - char *appdata = getenv ("APPDATA"); - - if (!appdata) - { - chsrc_error2 ("未能获取 APPDATA 环境变量"); - return NULL; - } - - char *config_dir = xy_2strcat(appdata, "\\uv\\"); - if (mkdir) - { - chsrc_ensure_dir (config_dir); - } - return xy_2strcat (config_dir, PL_Python_uv_ConfigFile); - } - else - { - /* config path on Linux or macOS */ - if (mkdir) - { - chsrc_ensure_dir (PL_Python_uv_User_ConfigPath); - } - return xy_2strcat (PL_Python_uv_User_ConfigPath, PL_Python_uv_ConfigFile); - } - } -} - -void -pl_python_uv_getsrc (char *option) -{ - char *uv_config = pl_python_find_uv_config (false); - - if (!chsrc_check_file (uv_config)) - { - chsrc_error2 ("未找到 uv 配置文件"); - return; - } - - /* 获取 [[index]] 配置项的 url */ - if (xy.on_windows) - { - /* 在 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); - } - else - { - /* 在类 Unix 系统上使用 grep */ - char *cmd = xy_str_gsub (RAWSTR_pl_python_get_uv_config, "@f@", uv_config); - chsrc_run (cmd, RunOpt_Default); - } -} - - -/** - * @consult https://docs.astral.sh/uv/configuration/files/ - * https://github.com/RubyMetric/chsrc/issues/139 - */ -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) - { - 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)) - { - /* 当 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 - { - cmd = xy_str_gsub (RAWSTR_pl_python_test_uv_if_set_source, "@f@", uv_config); - } - - 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 - { - chsrc_append_to_file (source_content, uv_config); - } - } - - if (chsrc_in_standalone_mode()) - { - chsrc_determine_chgtype (ChgType_Auto); - chsrc_conclude (&source); - } -} - - -void -pl_python_uv_resetsrc (char *option) -{ - pl_python_uv_setsrc (option); -} diff --git a/src/recipe/lang/Python/uv/README.md b/src/recipe/lang/Python/uv/README.md new file mode 100644 index 000000000..406a5b54a --- /dev/null +++ b/src/recipe/lang/Python/uv/README.md @@ -0,0 +1,77 @@ + + +# 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 优先使用 `$XDG_CONFIG_HOME/uv/uv.toml`,未设置时使用 `~/.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。 + +指定镜像代码时,若该代码同时提供 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]]`。读取时换行统一为 LF 处理,写入时按系统平台输出行尾(Windows 为 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/uv-helper.c b/src/recipe/lang/Python/uv/uv-helper.c new file mode 100644 index 000000000..2362b8697 --- /dev/null +++ b/src/recipe/lang/Python/uv/uv-helper.c @@ -0,0 +1,736 @@ +/** ------------------------------------------------------------ + * SPDX-License-Identifier: GPL-3.0-or-later + * ------------------------------------------------------------- + * Lib Authors : Mikachu2333 + * Contributors : Nul None + * | + * Created On : <2026-08-02> + * Last Modified : <2026-08-02> + * + * + * @file uv-helper.c + * @brief uv 配置 (uv.toml / pyproject.toml) 字符串改写工具 + * (由 uv.c #include, 可被 test 单独 #include) + * + * 仅处理 chsrc 关心的 uv 配置子集, 不是完整 TOML 解析器: + * - uv.toml: 顶层 [[index]] 数组表、顶层 python-install-mirror 键 + * - pyproject.toml: [tool.uv] 表内的 [[tool.uv.index]] 数组表、 + * python-install-mirror 键 + * + * 刻意不处理: 多行字符串、数组/内联表值、字符串转义 —— + * 因为所有改写都按“整行替换/删除”进行, 无需解析值本身。 + * 已知限制: 多行字符串 (`"""`) 内若出现行首 `[` 会被误认为段头, 罕见可忽略。 + * + * 所有函数均为 static, 依赖调用方所在翻译单元提供 stdlib/string.h。 + * ------------------------------------------------------------*/ + +#include +#include +#include +#include + +/** + * @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); + 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) +{ + while (*p == ' ' || *p == '\t') p++; + return p; +} + +/** + * @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) + { + 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) +{ + while (*p && *p != '\n') p++; + if (*p == '\n') p++; + return p; +} + +/** + * @brief 从 start 开始查找下一个段头行 (行首 '[') 的位置。 + * + * @param start 搜索起点 + * + * @return 下一个段头行的位置; 直到文件结尾都没有新段头时返回 NULL + */ +static const char * +uvh_find_section_end (const char *start) +{ + const char *p = start; + while (*p) + { + if (*uvh_skip_indent (p) == '[') return p; + p = uvh_next_line (p); + } + return NULL; +} + +/** + * @brief 全文件查找第一个行首匹配 header 的表头位置。 + * + * @param content 文件内容 + * @param header 要查找的表头文本 + * + * @return 匹配的表头行位置; 未找到返回 NULL + */ +static const char * +uvh_find_table (const char *content, const char *header) +{ + const char *p = content; + while (*p) + { + const char *line = uvh_skip_indent (p); + if (*line == '[' && uvh_header_match (line, header)) return p; + p = uvh_next_line (p); + } + return 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) +{ + for (const char *s = first; *s && (!end || s < end); s = uvh_next_line (s)) + { + if (uvh_key_prefix (uvh_skip_indent (s), key)) return s; + } + return 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) +{ + 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++; + + 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) +{ + 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] == '#'); +} + +/** + * @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) +{ + const char *first = NULL; + for (const char *s = content; *s; s = uvh_next_line (s)) + { + 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; +} + +/** + * @brief 用 new_line_text 替换 content 中从 line 开始的整行。 + * + * 保留原行缩进与后续内容; new_line_text 不含换行。中间改写统一使用 LF, + * 行尾由调用方在最终写盘前按系统平台转换 (Windows 为 CRLF, 其余 LF)。 + * + * @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) +{ + const char *line_end = strchr (line, '\n'); + if (!line_end) line_end = content + strlen (content); + + /* 复用原行缩进 */ + 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)(line - content), content); + pos += snprintf (ret + pos, len - pos, "%.*s", (int)ind, line); + pos += snprintf (ret + pos, len - pos, "%s", new_line_text); + strcpy (ret + pos, line_end); + return ret; +} + +/** + * @brief 在 insert_at 位置之前插入 insert_text (以 '\n' 结尾)。 + * + * 中间改写统一使用 LF, 行尾由调用方在最终写盘前按系统平台转换; + * 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) +{ + size_t head = insert_at - content; + + bool need_sep = (head > 0 && content[head - 1] != '\n'); + + size_t text_len = strlen (insert_text); + 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) + { + ret[pos++] = '\n'; + } + 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 值。 + * + * 优先修改 default = true 的 index (uv 的 PyPI 替代源), 不存在时回退到 + * 第一个; 作用域内无 index 段时新建, 父表不存在时一并创建。 + * + * @param content 原始内容 + * @param url 新的索引 URL + * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; + * 父表不存在时会在文件末尾创建父表并写入 index 段 + * + * @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) +{ + /* 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) + { + const char *table = uvh_find_table (content, parent_table); + if (!table) + { + /* 父表不存在: 在文件末尾创建父表并在其中创建 index 段, + * 中间改写统一 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 *ret = uvh_append_segment (content, seg); + free (seg); + return ret; + } + } + + /* 作用域内无 index 段: 追加到边界之前 */ + if (!ih) + { + 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, escaped_url); + free (escaped_url); + char *ret = uvh_insert_before (content, boundary, seg); + free (seg); + return ret; + } + + /* 段内第一行与段边界 (下一个段头或文件尾) */ + 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"); + + /* 段内没有 url 键: 在段内第一行之前插入 url (及缺失的 default) */ + if (!url_line) + { + bool has_default = uvh_find_key_in_section (first, end, "default") != NULL; + 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", 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 行 */ + 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\"", escaped_url); + char *ret = uvh_replace_line (content, url_line, new_line); + free (new_line); + free (escaped_url); + return ret; +} + + +/** + * @brief 替换/创建顶层键或指定表内键 (如 python-install-mirror) 的字符串值。 + * + * 键已存在时原地替换, 不存在时追加到作用域末尾; 父表不存在时一并创建。 + * + * @param content 原始内容 + * @param key 键名 + * @param url 新值 + * @param parent_table 父表头, 如 NULL (顶层) 或 "[tool.uv]"; + * 父表不存在时会在文件末尾创建父表并写入键 + * + * @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) +{ + const char *old_line = NULL; + const char *insert_at = NULL; + + if (parent_table) + { + const char *table = uvh_find_table (content, parent_table); + if (!table) + { + /* 父表不存在: 在文件末尾创建父表并写入键, + * 中间改写统一 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 *ret = uvh_append_segment (content, seg); + free (seg); + return ret; + } + + const char *first = uvh_next_line (table); + const char *key_zone_end = NULL; + for (const char *s = first; *s; s = uvh_next_line (s)) + { + const char *line = uvh_skip_indent (s); + if (*line == '[') + { + key_zone_end = s; + break; + } + } + 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) + { + 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); + } + + if (old_line) + { + /* 替换旧值行 */ + 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, escaped_url); + char *ret = uvh_replace_line (content, old_line, new_line); + free (new_line); + free (escaped_url); + return ret; + } + + /* 键不存在: 追加到作用域末尾 */ + 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, escaped_url); + char *ret = uvh_insert_before (content, insert_at, seg); + free (seg); + free (escaped_url); + return ret; +} + + +/** + * @brief 在指定表 (parent_table 为 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) +{ + const char *p = content; + if (parent_table) + { + 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; +} + + +/** + * @brief 提取 chsrc 管理的 index URL。 + * + * 优先 default = true 的 index, 否则取第一个; fully-qualified + * [[tool.uv.index]] 会隐式创建父路径, 不要求显式 [tool.uv]。 + * + * @param content 文件内容 + * @param index_header 段头文本, 如 "[[index]]" 或 "[[tool.uv.index]]" + * + * @return malloc 的 caller-free 解码后 URL; 未找到返回 NULL + * + * @memory SAFE + * return caller-free + */ +static char * +uvh_get_index_url (const char *content, const char *index_header) +{ + /* fully-qualified [[tool.uv.index]] 会隐式创建父路径,不要求显式 [tool.uv]。 */ + 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); +} diff --git a/src/recipe/lang/Python/uv/uv.c b/src/recipe/lang/Python/uv/uv.c new file mode 100644 index 000000000..0da1c3aab --- /dev/null +++ b/src/recipe/lang/Python/uv/uv.c @@ -0,0 +1,409 @@ +/** ------------------------------------------------------------ + * SPDX-License-Identifier: GPL-3.0-or-later + * ------------------------------------------------------------*/ + +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; + +// 调整 `python-install-mirror` 相关配置应在 `pl_uv_github_release` 处修改,本处仅为 uv 最常用的 `pypi` 镜像配置。 +void +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, "2026-08-02"); + + chef_set_chefs (this, 2, "@happy-game", "@MingriLingran"); + 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); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_But_Not_Implemented); + chef_set_default_scope (this, UserScope); + + chef_allow_english(this); + 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; +} + + +/** + * @consult: https://docs.astral.sh/uv/concepts/configuration-files + * chsrc get uv + * + * uv 配置文件选择顺序 (chsrc 写入策略): + * 1. ./uv.toml (项目级;与 pyproject.toml 并存时 uv 优先读取它) + * 2. ./pyproject.toml (项目级 [tool.uv] 段) + * 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" + +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 *file = xy_normalize_path (path); + FILE *f = fopen (file, "rb"); + if (!f) + { + free (file); + return NULL; + } + fclose (f); + + /* xy_file_read 会把 CRLF 与孤立 CR 统一为 LF。 */ + char *content = xy_file_read (file); + free (file); + return content; +} + + +char * +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 + { + if (xy.on_windows) + { + // config path on Windows + char *appdata = getenv ("APPDATA"); + + if (!appdata) + { + chsrc_error2 ("未能获取 APPDATA 环境变量"); + return NULL; + } + + char *config_dir = xy_path_join (appdata, "uv"); + if (mkdir) + { + chsrc_ensure_dir (config_dir); + } + 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 (config_dir); + } + char *result = xy_path_join (config_dir, PL_Python_uv_ConfigFile); + free (config_dir); + return result; + } + } +} + + +void +pl_python_uv_getsrc (char *option) +{ + char *uv_config = pl_python_find_uv_config (false); + + if (!uv_config || !chsrc_check_file (uv_config)) + { + if (!uv_config) + chsrc_error2 ("无法获取 uv 配置文件路径"); + else + chsrc_error2 ("未找到 uv 配置文件"); + free (uv_config); + return; + } + + // uv.toml 与 pyproject.toml 均使用同一套受限 TOML 读取逻辑。 + char *content = pl_python_uv_read_config (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); + if (url) + { + say (url); + free (url); + } + else + { + 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); + } + + char *mirror = uvh_get_value_in_table (content, "python-install-mirror", parent_table); + if (mirror) + { + say (mirror); + free (mirror); + } + + free (content); + free (uv_config); +} + + +/* + * Python下载镜像 (python-install-mirror) + * + * 内部 target, 不注册到 menu, 仅用于本 recipe 独立选择 Python 下载镜像。 + * 针对 python-build-standalone 的测速链接。 +*/ + +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.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; } + +void +pl_uv_github_release_prelude (void) +{ + chef_prep_this (pl_uv_github_release, gsr); + + 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); + + 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}, + {&CnbPython, "https://cnb.cool/astral-sh/python-build-standalone/-/releases/download", FeedByPrelude} + def_sources_end () + +#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 粗略测速 + chef_set_smURL_with_postfix (this, &Ustc, "/LatestRelease/SHA256SUMS"); + + // 中科大仅保留 Latest 且文件内含动态版本号, 使用模糊测速 + chef_set_provider_sm_accuracy (&Ustc, ROUGH); +} + + +/** + * 一次性完成uv配置文件的全部文件操作 (set 路径) + */ +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) + { + 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; +} + + +/** + * 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"); + + char *uv_config = pl_python_find_uv_config (true); + if (NULL == uv_config) + { + chsrc_error2 ("无法获取 uv 配置文件路径"); + return; + } + + /** + * reset: 把 index 与 python-install-mirror 写回 + * 默认上游(PyPI 与 python-build-standalone), 不依赖 .bak。 + */ + if (chsrc_in_reset_mode ()) + { + if (!chsrc_check_file (uv_config)) + { + chsrc_info ("没有 uv 配置文件,无需重置"); + free (uv_config); + return; + } + + 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); + if (!pl_python_uv_write_all (uv_config, default_pypi.url, default_gh.url)) + { + free (uv_config); + return; + } + free (uv_config); + + return; + } + + /** + * set: 选取源并写入。 + * 两个 URL 的语义不同:自定义 URL 只作为 PyPI index,Python 下载镜像仍自动测速。 + */ + char *pypi_opt = option; + char *gh_opt = NULL; + + if (option && !hp_is_url (option)) + { + 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, 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); + // 内部 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); + + chsrc_backup (uv_config); + 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()) + { + chsrc_determine_chgtype (ChgType_Auto); + chsrc_conclude (&source); + } +} + + +void +pl_python_uv_resetsrc (char *option) +{ + pl_python_uv_setsrc (option); +} diff --git a/src/recipe/menu.c b/src/recipe/menu.c index a5037f55b..5387e6f4d 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" diff --git a/src/recipe/recipe-template.c b/src/recipe/recipe-template.c index c25e96ca8..9dfe75e22 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 34de93294..9db5f70a6 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> *