├── src ├── main.rs ├── lib.rs ├── description.rs ├── toml.rs ├── args.rs ├── constants.rs ├── runtime.rs ├── cargo.rs ├── utils.rs └── registry.rs ├── CHANGELOG.md ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .github └── workflows │ ├── sync.yml │ └── release.yml ├── Cargo.toml ├── LICENSE-MIT ├── .gitignore ├── README.md └── LICENSE-Apache-2.0 /src/main.rs: -------------------------------------------------------------------------------- 1 | use crm::args::{handle_command, parse_args}; 2 | 3 | fn main() { 4 | handle_command(parse_args()); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | pub mod cargo; 3 | pub mod constants; 4 | pub mod description; 5 | pub mod registry; 6 | pub mod runtime; 7 | pub mod toml; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## v0.2.2 - 2024-09-05 4 | 5 | ### 新增功能 6 | 7 | - 更新项目依赖 8 | - 删除了 `hit` 镜像 (无法访问),并添加了新的镜像 9 | - 如果检测到 `.cargo/config` 文件,则提示更改为 `.cargo/config.toml` 10 | 11 | ### 对现有功能的更改 12 | 13 | - 更新了程序退出时的错误码,如果您有在自定义脚本中依赖了错误码,请及时更新脚本 14 | -------------------------------------------------------------------------------- /src/description.rs: -------------------------------------------------------------------------------- 1 | //! 镜像描述模块 2 | //! 3 | //! 该模块定义了 `RegistryDescription` 结构体 4 | 5 | /// 镜像描述 6 | #[derive(Debug)] 7 | pub struct RegistryDescription { 8 | /// 镜像地址 9 | pub registry: String, 10 | 11 | /// 镜像 `dl` 12 | pub dl: String, 13 | } 14 | 15 | impl RegistryDescription { 16 | /// 创建一个镜像描述对象 17 | pub fn new(registry: String, dl: String) -> Self { 18 | RegistryDescription { registry, dl } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "aliyun", 4 | "bfsu", 5 | "BIAO", 6 | "cernet", 7 | "CERNET", 8 | "CHUAN", 9 | "crmrc", 10 | "czvf", 11 | "dgst", 12 | "rsproxy", 13 | "rustcc", 14 | "sjtu", 15 | "softprops", 16 | "ureq", 17 | "ustc", 18 | "wearerequired" 19 | ], 20 | "rust-analyzer.checkOnSave": true 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync Mirror 2 | 3 | on: [ push, pull_request ] 4 | 5 | # 确保一次只运行一个镜像任务 6 | concurrency: 7 | group: git-mirror 8 | 9 | jobs: 10 | # 自动同步镜像 11 | git-mirror: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: wearerequired/git-mirror-action@v1 15 | env: 16 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 17 | with: 18 | source-repo: 'git@github.com:wtklbm/crm.git' 19 | destination-repo: 'git@gitee.com:wtklbm/crm.git' 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crm" 3 | version = "0.2.3" 4 | authors = ["wtklbm "] 5 | description = "crm can help you easy and fast switch between different cargo registries, now include: sjtu, tuna, ustc, rsproxy, bfsu, nju, hit, cqu, zju, CERNET." 6 | homepage = "https://github.com/wtklbm/crm" 7 | repository = "https://github.com/wtklbm/crm.git" 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["cargo", "registry"] 11 | exclude = [".vscode/**"] 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | opt-level = "z" 16 | panic = "abort" 17 | strip = true 18 | #lto = "fat" 19 | 20 | [profile.dev] 21 | codegen-units = 512 22 | 23 | [dependencies] 24 | toml_edit = "0.22.20" 25 | ureq = "2.10.1" 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wtklbm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/toml.rs: -------------------------------------------------------------------------------- 1 | //! 编译 `toml` 的模块 2 | //! 3 | //! 该模块用来解析 `toml` 文件,可以对 `toml` 文件进行更改,当更改完成之后可以再序列化为 `toml` 字符串。 4 | 5 | use std::{ 6 | fs::{create_dir_all, write}, 7 | path::Path, 8 | process, 9 | }; 10 | 11 | use toml_edit::{DocumentMut, Table, TomlError}; 12 | 13 | use crate::utils::to_out; 14 | 15 | #[derive(Debug)] 16 | pub struct Toml { 17 | /// 文档 18 | pub doc: DocumentMut, 19 | } 20 | 21 | impl Toml { 22 | /// 解析 `toml` 字符串 23 | pub fn parse(input: &str) -> Result { 24 | match input.parse::() { 25 | Ok(doc) => Ok(Toml { doc }), 26 | Err(e) => Err(e), 27 | } 28 | } 29 | 30 | /// 转换为不可变表 31 | pub fn table(&self) -> &Table { 32 | self.doc.as_table() 33 | } 34 | 35 | /// 转换为可变表 36 | pub fn table_mut(&mut self) -> &mut Table { 37 | self.doc.as_table_mut() 38 | } 39 | 40 | /// 转换为字符串 41 | pub fn toml_string(&self) -> String { 42 | self.doc.to_string().trim().to_string() 43 | } 44 | 45 | /// 写入到文件中 46 | pub fn write>(&self, path: P) { 47 | let parent = path.as_ref().parent().unwrap(); 48 | 49 | if !parent.is_dir() { 50 | create_dir_all(parent).unwrap(); 51 | } 52 | 53 | if let Err(e) = write(path, self.toml_string()) { 54 | to_out(format!("写入文件失败:\n {}", e)); 55 | process::exit(13); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Rust ### 2 | /target/ 3 | Cargo.lock 4 | /cargo-timing.html 5 | /cargo-timing-*.html 6 | 7 | ### VisualStudioCode ### 8 | .vscode/* 9 | !.vscode/settings.json 10 | !.vscode/tasks.json 11 | !.vscode/launch.json 12 | !.vscode/extensions.json 13 | *.code-workspace 14 | .VSCodeCounter/ 15 | 16 | # Ignore all local history of files 17 | .history 18 | .ionide 19 | 20 | # intellij 21 | .idea/ 22 | .idea_modules/ 23 | 24 | ### Windows ### 25 | # Windows thumbnail cache files 26 | Thumbs.db 27 | Thumbs.db:encryptable 28 | ehthumbs.db 29 | ehthumbs_vista.db 30 | 31 | # Dump file 32 | *.stackdump 33 | 34 | # Folder config file 35 | [Dd]esktop.ini 36 | 37 | # Recycle Bin used on file shares 38 | $RECYCLE.BIN/ 39 | 40 | # Windows Installer files 41 | *.cab 42 | *.msi 43 | *.msix 44 | *.msm 45 | *.msp 46 | 47 | # Windows shortcuts 48 | *.lnk 49 | 50 | ### macOS ### 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | ### Linux ### 79 | *~ 80 | 81 | # temporary files which can be created if a process still has a handle open of a deleted file 82 | .fuse_hidden* 83 | 84 | # KDE directory preferences 85 | .directory 86 | 87 | # Linux trash folder which might appear on any partition or disk 88 | .Trash-* 89 | 90 | # .nfs files are created when an open file is removed but is still being accessed 91 | .nfs* 92 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // https://github.com/vadimcn/codelldb/blob/master/MANUAL.md 5 | { 6 | "name": "Windows/*nux: 使用 LLDB 调试", 7 | "type": "lldb", 8 | "request": "launch", 9 | "cwd": "${workspaceRoot}", 10 | "program": "${cargo:program}", 11 | "preLaunchTask": "Rust: 构建开发版本", 12 | "cargo": { 13 | "args": ["build"], 14 | "problemMatcher": "$rustc", 15 | "env": { 16 | "RUST_BACKTRACE": "full" 17 | }, 18 | }, 19 | "console": "internalConsole" 20 | }, 21 | { 22 | "name": "Windows/*nux: 使用 GDB 调试", 23 | "type": "gdb", 24 | "request": "launch", 25 | "cwd": "${workspaceRoot}", 26 | "valuesFormatting": "parseText", 27 | "preLaunchTask": "Rust: 构建开发版本", 28 | "arguments": "", 29 | "env": { 30 | "RUST_BACKTRACE": "full" 31 | }, 32 | "target": "./target/debug/${workspaceFolderBasename}", 33 | "windows": { 34 | "target": "./target/debug/${workspaceFolderBasename}.exe" 35 | }, 36 | }, 37 | { 38 | "name": "Windows: 使用 MSVC 调试", 39 | "type": "cppvsdbg", 40 | "request": "launch", 41 | "stopAtEntry": false, 42 | "cwd": "${workspaceFolder}", 43 | "args": [], 44 | "environment": [ 45 | { "name": "RUST_BACKTRACE", "value": "full" } 46 | ], 47 | "console": "internalConsole", 48 | "program": "./target/debug/${workspaceFolderBasename}.exe", 49 | "preLaunchTask": "Rust: 构建开发版本" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | //! # 解析并处理程序运行时参数 2 | //! 3 | //! 当程序运行时,会使用一些参数来完成一系列的操作,当程序接收到命令行参数时, 4 | //! 由该模块完成对参数的解析和处理。 5 | //! 6 | //! 目前可接收的运行时参数主要包括: 7 | //! - `crm best`: 评估网络延迟并自动切换到最优的镜像 8 | //! - `crm best git`: 仅评估 git 镜像源 9 | //! - `crm best sparse`: 仅评估支持 sparse 协议的镜像源 10 | //! - `crm best git-download`: 仅评估能够快速下载软件包的 git 镜像源 (推荐使用) 11 | //! - `crm best sparse-download`: 仅评估能够快速下载软件包且支持 sparse 协议的镜像源 (推荐使用) 12 | //! - `crm current`: 获取当前所使用的镜像 13 | //! - `crm default`: 恢复为官方默认镜像 14 | //! - `crm install [args]`: 使用官方镜像执行 `cargo install` 15 | //! - `crm list`: 从镜像配置文件中获取镜像列表 16 | //! - `crm publish [args]`: 使用官方镜像执行 `cargo publish` 17 | //! - `crm remove `: 在镜像配置文件中删除镜像 18 | //! - `crm save
`: 在镜像配置文件中添加/更新镜像 19 | //! - `crm test [name]`: 下载测试包以评估网络延迟 20 | //! - `crm update [args]`: 使用官方镜像执行 `cargo update` 21 | //! - `crm use `: 切换为要使用的镜像 22 | //! - `crm version`: 查看当前版本 23 | //! - `crm check-update`: 检测版本更新 24 | //! 25 | //! 其中,`save`、`remove` 命令只修改 `${HOME}/.crmrc` 配置文件, 26 | //! 而不对 `${CARGO_HOME}/.cargo/config` 文件做任何的操作。 27 | //! 如果需要操作 `.crmrc` 镜像配置时仍要修改 `config` 配置文件, 28 | //! 请在操作完镜像配置文件后手动执行相应的操作镜像的命令。 29 | 30 | use std::env::args_os; 31 | 32 | use crate::{ 33 | constants::APP_VERSION, 34 | registry::Registry, 35 | utils::{get_newest_version, not_command, to_out}, 36 | }; 37 | 38 | type Args = (String, Vec); 39 | 40 | /// 解析程序运行时所传递的命令行参数 41 | /// 42 | /// 当参数解析完之后会返回一个元组,元组的第一项为要执行的命令, 43 | /// 元组的第二项为执行该命令所要用到的参数。 44 | pub fn parse_args() -> Args { 45 | let mut args_os = args_os().map(|os_string| os_string.into_string().unwrap()); 46 | 47 | args_os.next(); 48 | 49 | let command = args_os.next(); 50 | 51 | if command.is_none() { 52 | not_command(""); 53 | } 54 | 55 | ( 56 | command.unwrap(), 57 | args_os 58 | .map(|v| v.trim().to_string()) 59 | .collect::>(), 60 | ) 61 | } 62 | 63 | /// 根据运行时参数来处理要执行的命令 64 | /// 65 | /// 该函数传递一个运行时参数对象,其中包括命令和执行命令用到的参数。 66 | pub fn handle_command((command, args): Args) { 67 | let mut r = Registry::new(); 68 | 69 | match command.trim().to_lowercase().as_str() { 70 | // 列出镜像 71 | "list" => println!("{}", r.list(&r.current().0)), 72 | 73 | // 恢复默认镜像 74 | "default" => r.default(), 75 | 76 | // 切换镜像 77 | "use" => r.select(args.first()), 78 | 79 | // 删除镜像 80 | "remove" => r.remove(args.first()), 81 | 82 | // 使用官方镜像执行 `cargo publish` 83 | "publish" => r.publish(args.join(" ")), 84 | 85 | // 使用官方镜像执行 `cargo update` 86 | "update" => r.update(args.join(" ")), 87 | 88 | // 使用官方镜像执行 `cargo install` 89 | "install" => r.install(args.join(" ")), 90 | 91 | // 对镜像源网络延迟进行评估 92 | "test" => r.test(&r.current().0, args.first()), 93 | 94 | // 获取当前镜像 95 | "current" => { 96 | let (name, addr) = r.current(); 97 | 98 | match addr { 99 | Some(addr) => to_out(format!("{}: {}", name, addr)), 100 | None => to_out(name), 101 | }; 102 | } 103 | 104 | // 查看当前的版本 105 | "version" => { 106 | println!(" crm v{APP_VERSION}"); 107 | } 108 | 109 | // 检查版本更新 110 | "check-update" => { 111 | if let Some(newest) = get_newest_version() { 112 | if newest != APP_VERSION { 113 | return println!(" 检测到新版本: {newest},请切换到官方镜像源以执行更新"); 114 | } 115 | }; 116 | 117 | println!(" 暂无更新"); 118 | } 119 | 120 | command => { 121 | let (name, addr, dl) = (args.get(0), args.get(1), args.get(2)); 122 | 123 | match command { 124 | // 评估网络延迟并自动切换到最优的镜像 125 | "best" => r.best(name), 126 | 127 | // 添加/更新镜像 128 | "save" => r.save(name, addr, dl), 129 | 130 | _ => not_command(command), 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | //! # 常量 2 | //! 3 | //! 该模块会存储一些程序用到的常量。 4 | 5 | /// `${CARGO_HOME}/.cargo/config` 文件中的 `[source]` 属性 6 | pub const SOURCE: &str = "source"; 7 | 8 | pub const NET: &str = "net"; 9 | 10 | /// 由于网络原因,在项目添加 `dependencies` 后总显示无法连接 `github.com` 。但是终端中却可以正常使用 `git clone` 命令。 11 | /// 如果有遇到同样情况的,可以在 `$HOME/.cargo/config` 文件中内写入: 12 | /// 13 | /// ```toml 14 | /// [net] 15 | /// git-fetch-with-cli = true 16 | /// ``` 17 | /// 保存之后就解决了 `cargo` 无法连接 `github.com` 的错误 18 | /// @note 可以使用环境变量 `export CARGO_NET_GIT_FETCH_WITH_CLI="true"` 来进行覆盖 19 | /// @reference https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli 20 | pub const GIT_FETCH_WITH_CLI: &str = "git-fetch-with-cli"; 21 | 22 | /// `${CARGO_HOME}/.cargo/config` 文件中的 `[source.crates-io]` 属性 23 | pub const CRATES_IO: &str = "crates-io"; 24 | 25 | /// `${CARGO_HOME}/.cargo/config` 文件中的 `[source.crates-io]` 属性下面的 `replace-with` 属性 26 | pub const REPLACE_WITH: &str = "replace-with"; 27 | 28 | /// `${CARGO_HOME}/.cargo/config` 文件中的 `[source.xxx]` 属性下面的 `registry` 属性 29 | pub const REGISTRY: &str = "registry"; 30 | 31 | /// `${CARGO_HOME}/.cargo/config` 文件中的 `[registries.xxx]` 属性名 32 | pub const REGISTRIES: &str = "registries"; 33 | 34 | /// `cargo` 默认的镜像名 35 | pub const RUST_LANG: &str = "rust-lang"; 36 | 37 | /// `${HOME}` 目录下的 `.crmrc` 文件的文件名 38 | pub const CRMRC: &str = ".crmrc"; 39 | 40 | /// `${HOME}` 目录下的 `.crmrc` 文件的文件路径 41 | pub const CRMRC_PATH: &str = "~/.crmrc"; 42 | 43 | /// 用户查找 `"${CARGO_HOME}"` 环境变量 44 | pub const CARGO_HOME: &str = "CARGO_HOME"; 45 | 46 | /// `"CARGO"` 47 | pub const CARGO: &str = "cargo"; 48 | 49 | /// `${CARGO_HOME}` 目录下的 `.cargo` 文件夹 50 | pub const DOT_CARGO: &str = ".cargo"; 51 | 52 | /// `${CARGO_HOME}/.cargo` 目录下的 `config` 文件 53 | pub const CONFIG: &str = "config"; 54 | 55 | /// `${CARGO_HOME}/.cargo` 目录下的 `config.toml` 文件 (推荐) 56 | pub const CONFIG_TOML: &str = "config.toml"; 57 | 58 | /// `dl` 59 | pub const DL: &str = "dl"; 60 | 61 | /// 应用程序名称 62 | pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); 63 | 64 | /// 应用程序版本号 65 | pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); 66 | 67 | /// 表 68 | pub const TABLE: &str = "表"; 69 | 70 | /// 字符串 71 | pub const STRING: &str = "字符串"; 72 | 73 | /// 请修改/删除后重试 74 | pub const PLEASE_TRY: &str = "请修改/删除后重试"; 75 | 76 | /// `UNC` 路径前缀 77 | pub const UNC_PREFIX: &str = r"\\?\"; 78 | 79 | /// 默认镜像内容 80 | pub const CRMRC_FILE: &str = r#" 81 | # `crm` 配置 82 | 83 | # 官方源 84 | [source.rust-lang] 85 | registry = "https://github.com/rust-lang/crates.io-index" 86 | dl = "https://crates.io/api/v1/crates" 87 | 88 | # 上海交通大学 89 | [source.sjtu] 90 | registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index" 91 | dl = "https://mirror.sjtu.edu.cn/crates.io/crates/{crate}/{crate}-{version}.crate" 92 | 93 | # 上海交通大学 - sparse 94 | [source.sjtu-sparse] 95 | registry = "sparse+https://mirrors.sjtug.sjtu.edu.cn/crates.io-index/" 96 | dl = "https://mirror.sjtu.edu.cn/crates.io/crates/{crate}/{crate}-{version}.crate" 97 | 98 | # 中科大 99 | [source.ustc] 100 | registry = "https://mirrors.ustc.edu.cn/crates.io-index" 101 | dl = "https://crates-io.proxy.ustclug.org/api/v1/crates" 102 | 103 | # 中科大 - sparse 104 | [source.ustc-sparse] 105 | registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/" 106 | dl = "https://crates-io.proxy.ustclug.org/api/v1/crates" 107 | 108 | # 字节跳动 109 | [source.rsproxy] 110 | registry = "https://rsproxy.cn/crates.io-index" 111 | dl = "https://rsproxy.cn/api/v1/crates" 112 | 113 | # 字节跳动 - sparse 114 | [source.rsproxy-sparse] 115 | registry = "sparse+https://rsproxy.cn/index/" 116 | dl = "https://rsproxy.cn/api/v1/crates" 117 | 118 | # 清华大学 119 | [source.tuna] 120 | registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" 121 | dl = "https://crates.io/api/v1/crates" 122 | 123 | # 清华大学 - sparse 124 | [source.tuna-sparse] 125 | registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/" 126 | dl = "https://crates.io/api/v1/crates" 127 | 128 | # 北京外国语大学 129 | [source.bfsu] 130 | registry = "https://mirrors.bfsu.edu.cn/git/crates.io-index.git" 131 | dl = "https://crates.io/api/v1/crates" 132 | 133 | # 北京外国语大学 - sparse 134 | [source.bfsu-sparse] 135 | registry = "sparse+https://mirrors.bfsu.edu.cn/crates.io-index/" 136 | dl = "https://crates.io/api/v1/crates" 137 | 138 | # 南京大学 139 | [source.nju] 140 | registry = "https://mirror.nju.edu.cn/git/crates.io-index.git" 141 | dl = "https://crates.io/api/v1/crates" 142 | 143 | # 重庆大学 - sparse 144 | [source.cqu-sparse] 145 | registry = "sparse+https://mirrors.cqu.edu.cn/crates.io-index/" 146 | dl = "https://crates.io/api/v1/crates" 147 | 148 | # 浙江大学 - sparse 149 | [source.zju-sparse] 150 | registry = "sparse+https://mirrors.zju.edu.cn/crates.io-index/" 151 | dl = "https://crates.io/api/v1/crates" 152 | 153 | # CERNET聚合镜像 154 | [source.cernet] 155 | registry = "https://mirrors.cernet.edu.cn/crates.io-index.git" 156 | dl = "https://crates.io/api/v1/crates" 157 | 158 | # CERNET聚合镜像 - sparse 159 | [source.cernet-sparse] 160 | registry = "sparse+https://mirrors.cernet.edu.cn/crates.io-index/" 161 | dl = "https://crates.io/api/v1/crates" 162 | 163 | # 阿里云 - sparse 164 | [source.aliyun-sparse] 165 | registry = "sparse+https://mirrors.aliyun.com/crates.io-index/" 166 | dl = "https://crates.io/api/v1/crates" 167 | "#; 168 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: full 12 | RUST_LOG: trace 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | jobs: 19 | # 为每个操作系统构建源代码 20 | github_build: 21 | name: Build release binaries 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | include: 26 | # Windows 27 | - target: x86_64-pc-windows-msvc 28 | os: windows-latest 29 | name: crm_windows_amd64.tar.gz 30 | binary_name: crm.exe 31 | 32 | # # 以下构建会报错: 33 | # # Error: failed to run custom build command for `ring v0.16.20` 34 | # - target: aarch64-pc-windows-msvc 35 | # os: windows-latest 36 | # name: crm_windows_arm64.tar.gz 37 | # binary_name: crm.exe 38 | 39 | # macOS 40 | - target: x86_64-apple-darwin 41 | os: macOS-latest 42 | name: crm_darwin_amd64.tar.gz 43 | binary_name: crm 44 | 45 | # - target: aarch64-apple-darwin 46 | # os: macOS-latest 47 | # name: crm_darwin_arm64.tar.gz 48 | # binary_name: crm 49 | 50 | # Linux 51 | - target: x86_64-unknown-linux-gnu 52 | os: ubuntu-latest 53 | name: crm_linux_amd64.tar.gz 54 | binary_name: crm 55 | 56 | runs-on: ${{ matrix.os }} 57 | continue-on-error: true 58 | steps: 59 | - name: Setup | Checkout 60 | # https://github.com/actions/checkout 61 | uses: actions/checkout@v4 62 | 63 | # 在构建时缓存文件 64 | - name: Setup | Cache Cargo 65 | # https://github.com/actions/cache/blob/main/examples.md#rust---cargo 66 | uses: actions/cache@v4 67 | with: 68 | path: | 69 | ~/.cargo/bin/ 70 | ~/.cargo/registry/index/ 71 | ~/.cargo/registry/cache/ 72 | ~/.cargo/git/db/ 73 | target/ 74 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 75 | 76 | # 在 `rustup` 帮助下安装 Rust 工具链 77 | - name: Setup | Rust 78 | # https://github.com/actions-rs/toolchain 79 | uses: actions-rs/toolchain@v1 80 | with: 81 | toolchain: nightly 82 | override: true 83 | profile: minimal 84 | target: ${{ matrix.target }} 85 | 86 | - name: Build | Build 87 | # https://github.com/actions-rs/cargo 88 | uses: actions-rs/cargo@v1 89 | with: 90 | command: build 91 | args: --release --target ${{ matrix.target }} 92 | use-cross: ${{ matrix.os == 'ubuntu-latest' }} 93 | 94 | - name: Post Build | Prepare artifacts 95 | run: | 96 | cd target/${{ matrix.target }}/release || { 97 | echo "Cannot open \"target/${{ matrix.target }}/release\"" >&2 98 | } 99 | 100 | # 尝试剥离二进制文件的调试符号 (非Windows系统可能会失败) 101 | strip ${{ matrix.binary_name }} || true 102 | 103 | # 根据操作系统选择不同的压缩方式 104 | # 第一个参数为目标压缩包,第二个参数才是要压缩的文件 105 | if [[ "${{ matrix.os }}" == "windows-latest" ]]; then 106 | 7z a ../../../${{ matrix.name }} ${{ matrix.binary_name }} 107 | else 108 | tar czvf ../../../${{ matrix.name }} ${{ matrix.binary_name }} 109 | fi 110 | 111 | cd - &>/dev/null 112 | 113 | - name: Verify artifact exists 114 | run: | 115 | if [ ! -f "${{ matrix.name }}" ]; then 116 | echo "Error: Artifact file ${{ matrix.name }} does not exist, in \"$(pwd)\"" 117 | ls -ahl 118 | exit 1 119 | fi 120 | 121 | - name: Deploy | Upload artifacts 122 | uses: actions/upload-artifact@v4 123 | with: 124 | name: ${{ matrix.name }} 125 | path: ${{ matrix.name }} 126 | compression-level: 9 127 | 128 | # 使用 Rust 构建目标和发行说明创建 GitHub 发行版 129 | github_release: 130 | name: Create GitHub Release 131 | needs: github_build 132 | runs-on: ubuntu-latest 133 | permissions: 134 | contents: write 135 | steps: 136 | - name: Setup | Checkout 137 | uses: actions/checkout@v4 138 | with: 139 | fetch-depth: 0 140 | 141 | - name: Setup | Artifacts 142 | uses: actions/download-artifact@v4 143 | with: 144 | path: release-dist 145 | 146 | - name: Setup | Checksums 147 | run: | 148 | ls -ahl release-dist 149 | 150 | if [ ! -d "release-dist" ]; then 151 | echo "Error: release-dist directory does not exist, in \"$(pwd)\"" 152 | exit 1 153 | fi 154 | 155 | find release-dist -type f -print0 | while IFS= read -r -d '' file; do 156 | echo "Processing file: \"$file\"" 157 | openssl dgst -sha256 -r "$file" | awk '{print $1}' > "${file}.sha256" 158 | done 159 | 160 | echo "All files and checksums:" 161 | find release-dist -type f -exec ls -la {} \; 162 | 163 | - name: Publish 164 | # https://github.com/softprops/action-gh-release 165 | uses: softprops/action-gh-release@v2 166 | with: 167 | files: release-dist/**/* 168 | env: 169 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crm (Cargo registry manager) 2 | 3 | [`crm`](https://github.com/wtklbm/crm) 是一个在终端运行的镜像管理程序,能够对 `Cargo` 镜像源进行简单的添加、修改、删除操作,并能帮助您快速的切换不同的 `Cargo` 镜像源。`crm` 内置了多种国内 (中国) 镜像源,它们分别是:`sjtu`, `tuna`, `ustc`, `rsproxy`,`bfsu`, `nju`, `cqu`, `zju`, `cernet`。 4 | 5 | 6 | 在使用 Rust 语言做开发时,使用 Rust 官方镜像源进行 `cargo build` 的速度非常的慢,可能会因为网络的原因导致依赖下载超时而无法完成编译。为了能够在最少的时间内完成打包操作,一般会使用国内镜像源来代替官方镜像。 7 | 8 | 9 | 通常,大家一般会手动修改 `~/.cargo/config.toml` 文件来完成镜像的切换,手动修改配置文件的工作是繁琐的,它需要手动打开文件所在的目录,还要记住每一个镜像源的地址和配置方式,在不知道哪个国内源的网速最快的时候,我们还需要对镜像的速度进行手动的测速,在使用国内镜像源的过程中,如果当前所使用的国内镜像源也挂了,我们还需要切换到另一个国内镜像源,这就显得非常的棘手。如果您手动配置了国内镜像源,并且还经常的通过 `cargo publish` 发包的话 ,那么在发包之前,还需要将国内镜像源再手动切换为官方镜像。在比如,每一个国内镜像源同步镜像的时间是不一样的,如果您刚发了一个包并且想第一时间应用到您的项目中,但是因为国内镜像源的没有及时的同步镜像,而导致包无法下载,这个时候您还需要切换到官方镜像源来下载最新发布的包。每一次手动切换镜像的操作都是繁琐且耗时的,而 `crm` 就是为了解决上述的问题。 10 | 11 | 12 | 13 | ## 安装 14 | 15 | ### 通过 `cargo` 安装 16 | 17 | ```bash 18 | # 在终端中执行 19 | # NOTE 最好使用官方镜像源来安装或升级 20 | 21 | cargo install crm 22 | ``` 23 | 24 | 25 | 26 | ### 在 `Arch Linux` 上安装 27 | 28 | 您可以从 Arch Linux 用户仓库 (AUR) 中安装它,感谢 [taotieren](https://github.com/taotieren) 为此做出的努力。 29 | 30 | - 31 | 32 | ```bash 33 | # 在终端中执行 34 | 35 | # `yay` 是一个 AUR 包管理器,可以使用您喜欢的任何包管理器来安装它,例如 `paru` 36 | yay -S crm 37 | ``` 38 | 39 | - 40 | 41 | ```bash 42 | # 在终端中执行 43 | 44 | yay -S crm-git 45 | ``` 46 | 47 | > NOTE:`crm` 和 `crm-git` 都是基于源代码来构建二进制文件的,如果不想通过下载源代码的方式来构建,则可以到 [Github Release](https://github.com/wtklbm/crm/releases) 中下载预构建的二进制版本。 48 | 49 | 50 | 51 | ### 手动下载预编译的二进制文件 52 | 53 | 可以直接从 [Github Release](https://github.com/wtklbm/crm/releases) 下载预编译的二进制版本,将其下载后,保存到任意目录,然后将该目录放到操作系统的 PATH 环境变量中: 54 | - 如果使用的是 Windows 11 系统,则可以在搜索框中搜索 "**编辑系统环境变量**",然后找到 "**环境变量**",找到用户变量中的 "**Path**",双击它,并添加自定义路径 55 | - 如果使用的是 macOS 或 Linux 系统,则可以将 `export PATH="$PATH:自定义路径"` 添加到 `.zshrc` 或 `.bashrc` 文件中 (取决于使用的是哪种 Shell) 56 | 57 | 通过这种安装方式,可以更加自由的管理该软件,半年甚至一年不更新也可以正常使用,可以用 `crm check-update` 来查询新版本,并进行更新。若要将 `crm` 批量安装到多台操作系统的话,这样会更加方便快捷。 58 | 59 | 在下载二进制文件时,可能会遇到下载失败的情况,为此,可以使用 Github 镜像来加速下载。 60 | 61 | ```bash 62 | # 在终端中执行 63 | 64 | # 使用 `https://hub.gitmirror.com/` 镜像来加速下载 65 | # 下载哪个版本,就将下面的 URL 部分进行相应的替换即可 66 | curl -O https://hub.gitmirror.com/https://github.com/wtklbm/crm/releases/download/v0.2.1/crm_windows_amd64.tar.gz 67 | ``` 68 | 69 | 70 | 71 | ## 使用 72 | 73 | `crm` 的原则是使用最小依赖,并尽可能的简化终端操作。您只需要在终端键入 `crm` 即可获得命令帮助信息。 74 | 75 | ```bash 76 | # 在终端执行 77 | # 78 | # NOTE: 79 | # - [args] 表示 args 是一个或多个可选参数 80 | # - 表示 name 是一个必填参数 81 | # 82 | # 下面这些命令在执行时会自动切换为官方镜像,避免了手动切换镜像的麻烦: 83 | # - `crm install` 对应 `cargo install` 84 | # - `crm publish` 对应 `cargo publish` 85 | # - `crm update` 对应 `cargo update` 86 | # 87 | # `crm test` 命令一般用于进行全量测试,而 `crm best` 是切换到最优镜像的快速方式 88 | 89 | $ crm 90 | 91 | crm best 评估网络延迟并自动切换到最优的镜像 92 | crm best git 仅评估 git 镜像源 93 | crm best sparse 仅评估支持 sparse 协议的镜像源 94 | crm best git-download 仅评估能够快速下载软件包的 git 镜像源 (推荐使用) 95 | crm best sparse-download 仅评估能够快速下载软件包且支持 sparse 协议的镜像源 (推荐使用) 96 | crm default 恢复为官方默认镜像 97 | crm install [args] 使用官方镜像执行 "cargo install" 98 | crm list 从镜像配置文件中获取镜像列表 99 | crm publish [args] 使用官方镜像执行 "cargo publish" 100 | crm remove 在镜像配置文件中删除镜像 101 | crm save
在镜像配置文件中添加/更新镜像 102 | crm test [name] 下载测试包以评估网络延迟 103 | crm update [args] 使用官方镜像执行 "cargo update" 104 | crm use 切换为要使用的镜像 105 | crm version 查看当前版本 106 | crm check-update 检测版本更新 107 | ``` 108 | 109 | 110 | - 根据 [反馈](https://github.com/wtklbm/crm/issues/11),并不建议您使用 `ustc` 镜像源,它会限制并发 111 | - 以下镜像只能在更新 `git` 镜像仓库时获得加速效果,在下载包时无法获得加速效果: 112 | - `tuna` 113 | - `bfsu` 114 | - `nju` 115 | - ... 一切 `dl` 的值为 `https://crates.io/api/v1/crates` 的镜像都无法获得加速 (有关镜像详情,请参见 [constants.rs](https://github.com/wtklbm/crm/blob/main/src/constants.rs#L80) 文件) 116 | 117 | 118 | 119 | ## 在项目中使用来自不同镜像源的依赖 120 | 121 | `crm` 在配置镜像源时,会默认在 `~/.cargo/config.toml` 中多增加一个 `registries` 属性对象,通过增加该属性对象,您就可以在项目中应用来自于不同镜像源的依赖。比如您在使用官方镜像源时,可以通过在项目的 `Cargo.toml` 文件中指定依赖的 `registry` 属性来使用不同的国内镜像源。如果您已经在使用国内镜像源了,那么也可以通过修改 `registry` 属性的方式来切换到其他的国内镜像源。以下是一个示例。 122 | 123 | 124 | 125 | 126 | ```toml 127 | # Cargo.toml 128 | 129 | # 使用官方镜像源时,`registry` 属性可选的值为: `sjtu`, `tuna`, `ustc`, `rsproxy`, `bfsu`, `nju` ... 130 | # 如果您已经使用 `crm` 切换到了 `rsproxy` 镜像源,那么 `registry` 属性可选的值则为其他国内镜像:`sjtu`, `tuna`, `ustc`, `bfsu`, `nju` ... 131 | # 以此类推 132 | # 值得注意的是,在使用国内镜像源时,您无法直接通过修改 `registry` 属性的方式使用官方镜像源 133 | # 如果您想使用官方镜像源,那么请在终端执行 `crm default` 来切换到官方镜像 134 | 135 | [dependencies] 136 | # 使用 `ustc` 国内镜像来下载 `log` 依赖 137 | log = {version = "0.4.12", registry = "ustc"} 138 | 139 | # 使用 `sjtu` 国内镜像来下载 `lazy_static` 依赖 140 | lazy_static = {version = "1.4.0", registry = "sjtu"} 141 | ``` 142 | 143 | 144 | 145 | > NOTE: 146 | > - 如果您刚安装 `crm`,那么请在终端执行一次:`crm default`,然后就可以在项目的 `Cargo.toml` 文件中配置 `registry` 属性了。 147 | > - 从 Rust v1.39.0 版本开始,`~/.cargo/config.toml` 将被推荐使用,如果 `~/.cargo/config` 和 `~/.cargo/config.toml` 同时存在,则优先使用 `~/.cargo/config` 配置。 148 | 149 | 150 | 151 | ## 使用 `sparse` 协议 152 | 153 | 在 `v1.68.0` 版本中,新增了一个 `sparse` 协议。以前我们更新注册表默认都是通过克隆 `git` 仓库来实现的,在克隆 `git` 仓库时会出现明显的延迟,`sparse` 协议的好处就是不用再克隆镜像源仓库了,而是仅通过 HTTPS 从网络上查询和下载您所需要的包。虽然该协议避免了 `git` 克隆的步骤,但是当镜像源的网络不稳定时较容易出错。 154 | 155 | 按照官方说法,使用该协议有两种方式: 156 | 157 | 1. 将 `export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse` 加入到 `.bashrc` 或 `.zshrc` 文件中 158 | 2. 或者编辑 `~/.cargo/config.toml` 文件: 159 | ```bash 160 | [registries.crates-io] 161 | protocol = "sparse" 162 | ``` 163 | 164 | 但是,当您使用 `crm` 时,`crm` 自带了一些支持 `sparse` 协议的镜像源,它们是开箱即用的。只须执行 `crm best sparse` 或 `crm best sparse-download` 即可轻松切换到支持 `sparse` 协议的镜像源。 165 | 166 | 如果您想了解更多的内容,请参考下面的链接: 167 | - 168 | - 169 | - 170 | - 171 | - 172 | 173 | 174 | 175 | 176 | ## 注意事项 177 | 178 | 1. `v0.1.0` 版本以下的 `.crmrc` 配置文件和最新版本的配置文件并不能相互兼容,如果您正在使用小于 `v0.1.0` 的版本,当您更新到最新版本时,请手动删除 `~/.crmrc` 文件 179 | 2. `crm` 会修改 `~/.cargo/config.toml` 文件来进行镜像源的切换,如果您使用的是小于 `v0.1.3` 的版本,那么当您使用 `crm` 切换镜像时,`~/.cargo/config.toml` 文件中的文档注释会被删除并且永远无法恢复,如果您在 `~/.cargo/config.toml` 文件中保存了笔记或者文档,请尽快更新到最新版,在最新版中,对此进行了优化,不再自动删除文档注释 (除修改的字段外) 180 | 3. `crm` 默认会在 `~/.cargo/config.toml` 文件中增加一个 `env.git-fetch-with-cli` 属性,值为 `true`,在使用 `crm` 时您无法删除该选项,如果您不想使用 `Git` 可执行文件进行 `Git` 操作,请手动修改 `~/.cargo/config.toml` 文件并将 `git-fetch-with-cli` 的值修改为 `false` 181 | 182 | 183 | 184 | 185 | ## 错误处理 186 | 187 | 以下是 crm 异常结束的情况对照表: 188 | - 1: 写入的镜像名称有误 189 | - 2: 写入的镜像地址有误 190 | - 3: 写入的 `dl` 字段值有误 191 | - 4: 命令无效 192 | - 5: `config.toml` 配置文件解析失败 (格式有误或字段缺失) 193 | - 6: 属性名错误 194 | - 7: 不能删除内置镜像 195 | - 8: 要删除的镜像不存在 196 | - 9: 传入的参数错误 197 | - 10: 要进行下载测试的镜像不存在 198 | - 11: 要进行连接测试的镜像不存在 199 | - 12: `.crmrc` 配置文件解析失败 (格式有误或字段缺失) 200 | - 13: 写入文件失败,请检查权限 201 | - 14: 配置文件冲突,需手动检查 202 | 203 | 204 | 205 | 206 | ## Others 207 | 208 | ### rust-library-chinese 209 | 210 | `rust-library-chinese` 是 Rust 核心库和标准库的源码级中文翻译,可以用作 IDE 工具的中文智能提示,也可以基于翻译好的内容生成 Rust 中文 API 文档。 211 | 212 | - [从 Github 访问](https://github.com/wtklbm/rust-library-i18n) 213 | - [从 Gitee 访问](https://gitee.com/wtklbm/rust-library-chinese) 214 | 215 | 216 | 217 | 218 | ## LICENSE 219 | 220 | MIT OR Apache-2.0 221 | 222 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! # 运行时 2 | //! 3 | //! `runtime` 是一个处理程序运行时配置的模块,通过它,我们可以通过执行的命令来更改 `.crmrc` 文件。 4 | //! 而 `.crmrc` 文件里面存储的是关于 `Cargo` 配置的相关信息。 5 | 6 | use std::{ 7 | collections::{btree_map::Iter, BTreeMap}, 8 | fs::read_to_string, 9 | iter::Chain, 10 | path::PathBuf, 11 | process, 12 | }; 13 | 14 | use toml_edit::{table, value, Item, Table}; 15 | 16 | use crate::{ 17 | constants::{ 18 | CRMRC, CRMRC_FILE, CRMRC_PATH, DL, PLEASE_TRY, REGISTRY, RUST_LANG, SOURCE, TABLE, 19 | }, 20 | description::RegistryDescription, 21 | toml::Toml, 22 | utils::{append_end_spaces, home_dir, status_prefix, to_out}, 23 | }; 24 | 25 | /// 运行时配置 26 | #[derive(Debug)] 27 | pub struct RuntimeConfig { 28 | /// 运行时配置的存放路径 29 | path: PathBuf, 30 | 31 | /// 用户自定义的配置 32 | config: Toml, 33 | 34 | /// 用户自定义镜像的映射表 35 | extend: BTreeMap, 36 | 37 | /// 默认镜像的映射表 38 | default: BTreeMap, 39 | } 40 | 41 | impl RuntimeConfig { 42 | /// 创建运行时配置对象 43 | pub fn new() -> Self { 44 | // 获取运行时配置的保存路径 45 | let rc_path = home_dir().join(CRMRC); 46 | 47 | // 获取用户自定义镜像配置 48 | let data = match read_to_string(&rc_path) { 49 | Ok(content) => content, 50 | Err(_) => String::new(), 51 | }; 52 | 53 | let extend = RuntimeConfig::parse(&data); 54 | let default = RuntimeConfig::parse(CRMRC_FILE); 55 | 56 | RuntimeConfig { 57 | extend: RuntimeConfig::extract_to_map(&extend), 58 | default: RuntimeConfig::extract_to_map(&default), 59 | path: rc_path, 60 | config: extend, 61 | } 62 | } 63 | 64 | /// 获取所有的镜像名 `Vec` 65 | pub fn registry_names(&self) -> Vec { 66 | self.default 67 | .iter() 68 | .chain(self.extend.iter()) 69 | .map(|(k, _)| k.to_string()) 70 | .collect() 71 | } 72 | 73 | /// 将运行时配置中的镜像列表转换为字符串 74 | pub fn to_string(&self, current: &String, sep: Option<&str>) -> String { 75 | let sep = if sep.is_none() { 76 | "" 77 | } else { 78 | Option::unwrap(sep) 79 | }; 80 | 81 | self.iter() 82 | .fold(String::new(), |mut memo, (k, v)| { 83 | let p = status_prefix(k, current); 84 | let k = append_end_spaces(k, None); 85 | memo.push_str(format! {"{}{}{}{}\n", p, k, sep, v.registry }.as_str()); 86 | memo 87 | }) 88 | .trim_end() 89 | .to_string() 90 | } 91 | 92 | /// 将运行时配置中的镜像列表名转换为字符串 93 | pub fn to_key_string(&self) -> String { 94 | let f = |key| format!(" - {}", key); 95 | let v1 = self.default.keys().map(f); 96 | let v2 = self.extend.keys().map(f); 97 | 98 | v1.chain(v2).collect::>().join("\n") 99 | } 100 | 101 | /// 将运行时配置写入到文件中 102 | pub fn write(&mut self) { 103 | self.convert_from_map(); 104 | self.config.write(&self.path); 105 | } 106 | 107 | /// 获取运行时配置中的某一个属性 108 | pub fn get(&self, registry_name: &str) -> Option<&RegistryDescription> { 109 | match self.get_extend(registry_name) { 110 | None => self.get_default(registry_name), 111 | v => v, 112 | } 113 | } 114 | 115 | /// 获取用户自定义运行时配置 116 | pub fn get_extend(&self, registry_name: &str) -> Option<&RegistryDescription> { 117 | self.extend.get(registry_name) 118 | } 119 | 120 | /// 获取默认的运行时配置 121 | pub fn get_default(&self, registry_name: &str) -> Option<&RegistryDescription> { 122 | self.default.get(registry_name) 123 | } 124 | 125 | /// 添加/更新运行时配置中的属性 126 | pub fn save(&mut self, registry_name: &str, registry_addr: &str, registry_dl: &str) { 127 | self.extend.insert( 128 | registry_name.to_string(), 129 | RegistryDescription { 130 | registry: registry_addr.to_string(), 131 | dl: registry_dl.to_string(), 132 | }, 133 | ); 134 | } 135 | 136 | /// 删除运行时配置中的属性 137 | pub fn remove(&mut self, registry_name: &str) { 138 | self.extend.remove(registry_name); 139 | } 140 | 141 | /// 将镜像名称和镜像地址收集到元祖中,并返回一个元祖数组 142 | pub fn to_tuples(&self, exclude_name: Option<&str>) -> Vec<(&str, &str)> { 143 | self.iter().fold(vec![], |mut memo, (k, v)| { 144 | if k.eq(RUST_LANG) || (exclude_name.is_some() && k.eq(exclude_name.unwrap())) { 145 | return memo; 146 | } 147 | 148 | memo.push((k, &v.registry)); 149 | memo 150 | }) 151 | } 152 | 153 | /// 创建迭代器 154 | fn iter(&self) -> Chain, Iter> { 155 | self.default.iter().chain(self.extend.iter()) 156 | } 157 | 158 | /// 将字符串解析为 `Toml` 对象 159 | fn parse(data: &str) -> Toml { 160 | let config = Toml::parse(data); 161 | 162 | if config.is_err() { 163 | to_out(format!("解析 {} 文件失败,{}", CRMRC_PATH, PLEASE_TRY)); 164 | process::exit(12); 165 | } 166 | 167 | let mut config = config.unwrap(); 168 | let data: &mut Table = config.table_mut(); 169 | if data.contains_key(SOURCE) { 170 | let source: &Item = &data[SOURCE]; 171 | if !source.is_table() { 172 | to_out(format!( 173 | "{} 文件中的 {} 字段不是一个{},{}", 174 | CRMRC_PATH, SOURCE, TABLE, PLEASE_TRY 175 | )); 176 | process::exit(12); 177 | } 178 | } else { 179 | data[SOURCE] = table(); 180 | }; 181 | 182 | config 183 | } 184 | 185 | /// 从配置转换为 `BTreeMap` 186 | fn extract_to_map(config: &Toml) -> BTreeMap { 187 | let data = config.table(); 188 | let source = data[SOURCE].as_table().unwrap(); 189 | let mut map = BTreeMap::new(); 190 | 191 | source 192 | .iter() 193 | .for_each(|(key, value)| match value.as_table() { 194 | Some(v) => { 195 | let r = v[REGISTRY].as_str(); 196 | let d = v[DL].as_str(); 197 | 198 | if r.is_none() || d.is_none() { 199 | to_out(format!( 200 | "{} 文件中的 [{}.{}] 里没有包含 {} 或 {} 字段, {}", 201 | CRMRC_PATH, SOURCE, key, REGISTRY, DL, PLEASE_TRY 202 | )); 203 | process::exit(12); 204 | } 205 | 206 | let registry = r.unwrap().to_string(); 207 | let dl = d.unwrap().to_string(); 208 | 209 | map.insert(key.to_string(), RegistryDescription::new(registry, dl)); 210 | } 211 | None => { 212 | to_out(format!( 213 | "{} 文件中的 {} 字段不是一个 {}, {}", 214 | CRMRC_PATH, key, TABLE, PLEASE_TRY 215 | )); 216 | process::exit(12); 217 | } 218 | }); 219 | 220 | map 221 | } 222 | 223 | /// 从 `BTreeMap` 转换为配置 224 | fn convert_from_map(&mut self) { 225 | let config = self.config.table_mut(); 226 | config[SOURCE] = table(); 227 | let source = config[SOURCE].as_table_mut().unwrap(); 228 | 229 | self.extend.iter().for_each(|(k, v)| { 230 | let RegistryDescription { registry, dl } = v; 231 | 232 | source[k] = table(); 233 | source[k][REGISTRY] = value(registry.to_string()); 234 | source[k][DL] = value(dl.to_string()); 235 | }); 236 | } 237 | } 238 | 239 | impl Default for RuntimeConfig { 240 | fn default() -> Self { 241 | Self::new() 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | //! # 处理 `Cargo` 配置 2 | //! 3 | //! 该模块会解析 `Cargo` 配置,并将其反序列化为对象的形式,修改完成后再序列化为相应的文件。 4 | //! `CargoConfig` 是一个操作 `Cargo` 配置文件的对象,有了它一切都好办了。 5 | 6 | use std::process; 7 | 8 | use toml_edit::{table, value, Item, Table}; 9 | 10 | use crate::{ 11 | constants::{ 12 | CRATES_IO, GIT_FETCH_WITH_CLI, NET, PLEASE_TRY, REGISTRIES, REGISTRY, REPLACE_WITH, 13 | RUST_LANG, SOURCE, STRING, TABLE, 14 | }, 15 | description::RegistryDescription, 16 | toml::Toml, 17 | utils::{cargo_config_path, field_eprint, get_cargo_config, to_out}, 18 | }; 19 | 20 | /// 验证字段是否存在 21 | fn verify_field_exists(data: &mut Table, key: &str) { 22 | if data.contains_key(key) { 23 | if !data[key].is_table() { 24 | field_eprint(key, TABLE); 25 | process::exit(5); 26 | } 27 | } else { 28 | data[key] = table(); 29 | }; 30 | } 31 | 32 | /// `Cargo` 配置对象 33 | #[derive(Debug)] 34 | pub struct CargoConfig { 35 | /// 配置对象中的数据,它是一个经过反序列化的对象 36 | data: Toml, 37 | } 38 | 39 | impl CargoConfig { 40 | /// 创建配置对象 41 | pub fn new() -> Self { 42 | let toml = get_cargo_config(); 43 | 44 | match Toml::parse(&toml) { 45 | Ok(mut config) => { 46 | let data: &mut Table = config.table_mut(); 47 | 48 | // 如果没有则创建表,否则判断是不是表 49 | verify_field_exists(data, SOURCE); 50 | verify_field_exists(data, REGISTRIES); 51 | verify_field_exists(data, NET); 52 | 53 | let net = data[NET].as_table().unwrap(); 54 | 55 | if !net.contains_key(GIT_FETCH_WITH_CLI) { 56 | data[NET][GIT_FETCH_WITH_CLI] = value(true); 57 | } 58 | 59 | CargoConfig { data: config } 60 | } 61 | 62 | Err(_) => { 63 | to_out(format!( 64 | "{} 文件解析失败,{}", 65 | cargo_config_path().display(), 66 | PLEASE_TRY 67 | )); 68 | process::exit(5); 69 | } 70 | } 71 | } 72 | 73 | /// 将 `Cargo` 配置写入到文件中 74 | pub fn make(&self) { 75 | self.data.write(cargo_config_path()) 76 | } 77 | 78 | /// 如果 `Cargo` 配置文件中不包含 `[source.crates-io]` 属性,则为 `Cargo` 配置自动填充。 79 | fn fill_crates_io(&mut self) { 80 | let data: &mut Table = self.data.table_mut(); 81 | let source: &mut Table = data[SOURCE].as_table_mut().unwrap(); 82 | 83 | if source.contains_key(CRATES_IO) { 84 | if !source[CRATES_IO].is_table() { 85 | field_eprint(format!("[{SOURCE}.{CRATES_IO}]"), TABLE); 86 | process::exit(5); 87 | } 88 | } else { 89 | data[SOURCE][CRATES_IO] = table(); 90 | } 91 | } 92 | 93 | /// 如果切换为默认镜像时,则删除 `replace_with` 属性。否则, 94 | /// 则为 `[source.creates-io]` 添加 `replace-with` 属性, 95 | /// 该属性用于指示要使用的外部镜像的名称。 96 | fn replace_with(&mut self, registry_name: &str) { 97 | self.fill_crates_io(); 98 | 99 | let data: &mut Table = self.data.table_mut(); 100 | let crates_io: &mut Item = &mut data[SOURCE][CRATES_IO]; 101 | 102 | // 去除属性 103 | if registry_name.eq(RUST_LANG) && !crates_io.is_none() { 104 | crates_io.as_table_mut().unwrap().remove(REPLACE_WITH); 105 | return; 106 | } 107 | 108 | // 追加属性 109 | crates_io[REPLACE_WITH] = value(registry_name); 110 | } 111 | 112 | /// 从 `Cargo` 配置文件中获取正在使用的镜像,其中 `rust-lang` 是 `Cargo` 默认使用的镜像。 113 | pub fn current(&mut self) -> (String, Option) { 114 | let data = self.data.table_mut(); 115 | let source = data[SOURCE].as_table_mut().unwrap(); 116 | 117 | // 如果 `source` 不包含 `CRATES_IO` 键,则初始化它 118 | if !source.contains_key(CRATES_IO) { 119 | source[CRATES_IO] = table(); 120 | } 121 | 122 | let name = { 123 | let crates_io = source[CRATES_IO].as_table().unwrap(); 124 | 125 | // 从配置文件中获取镜像名 126 | if crates_io.contains_key(REPLACE_WITH) { 127 | match crates_io[REPLACE_WITH].as_value().unwrap().as_str() { 128 | Some(name) => name, 129 | None => { 130 | field_eprint( 131 | format!("[{SOURCE}.{CRATES_IO}] 下的 {REPLACE_WITH}"), 132 | STRING, 133 | ); 134 | process::exit(5); 135 | } 136 | } 137 | } else { 138 | RUST_LANG 139 | } 140 | }; 141 | 142 | if !source.contains_key(name) { 143 | return (name.to_string(), None); 144 | } 145 | 146 | let source_name = source[name].as_table().unwrap(); 147 | 148 | if !source_name.contains_key(REGISTRY) { 149 | return (name.to_string(), None); 150 | } 151 | 152 | let addr = source_name[REGISTRY].as_str().map(|v| v.to_string()); 153 | 154 | (name.to_string(), addr) 155 | } 156 | 157 | /// 追加属性 158 | fn append_attribute(&mut self, key: &str, registry_name: &str, addr: &str) { 159 | let config: &mut Table = self.data.table_mut(); 160 | let source: &mut Item = &mut config[key]; 161 | 162 | match source.get(registry_name) { 163 | Some(x) => { 164 | if !x.is_table() { 165 | field_eprint(registry_name, TABLE); 166 | process::exit(5); 167 | } 168 | } 169 | None => source[registry_name] = table(), 170 | }; 171 | 172 | let attr = match key { 173 | SOURCE => REGISTRY, 174 | REGISTRIES => "index", 175 | _ => { 176 | to_out(format!("{key} 不是预期的属性名")); 177 | process::exit(6); 178 | } 179 | }; 180 | 181 | // 不管之前存在的值是什么,都要替换成新的值 182 | source[registry_name][attr] = value(addr.to_string()); 183 | } 184 | 185 | /// 在 `Cargo` 配置文件中添加新的 `[source.xxx]` 镜像属性,并为其指定 `registry` 属性。 186 | /// `registry` 属性是强制添加的,`${CARGO_HOME}/.cargo/config` 文件中如果存在则会覆盖。 187 | fn append_registry(&mut self, registry_name: &str, addr: String) { 188 | self.append_attribute(SOURCE, registry_name, &addr); 189 | } 190 | 191 | /// 在 `Cargo` 配置文件中添加新的 `[registries.xxx]` 镜像属性,并为其指定 `index` 属性。 192 | /// `index` 属性是强制添加的,`${CARGO_HOME}/.cargo/config` 文件中如果存在则会覆盖。 193 | fn append_registries(&mut self, remaining_registries: &[(&str, &str)]) { 194 | remaining_registries 195 | .iter() 196 | .for_each(|(registry_name, registry_addr)| { 197 | self.append_attribute(REGISTRIES, registry_name, registry_addr); 198 | }); 199 | } 200 | 201 | /// 删除老的属性 202 | fn remove_attribute(&mut self, key: &str, registry_name: &str) { 203 | if registry_name.eq(RUST_LANG) { 204 | return; 205 | } 206 | 207 | let source: &mut Item = &mut self.data.table_mut()[key]; 208 | 209 | // 如果没有 `[source.xxx]` 属性 210 | if let None = source.get(registry_name) { 211 | return; 212 | } 213 | 214 | source 215 | .as_table_mut() 216 | .unwrap() 217 | .remove(registry_name) 218 | .unwrap(); 219 | } 220 | 221 | /// 根据镜像名删除 `config` 中的旧的镜像属性 222 | fn remove_old_registry(&mut self, registry_name: &str) { 223 | self.remove_attribute(SOURCE, registry_name); 224 | } 225 | 226 | fn remove_old_registries(&mut self, remaining_registries: &[(&str, &str)]) { 227 | remaining_registries.iter().for_each(|(registry_name, _)| { 228 | self.remove_attribute(REGISTRIES, registry_name); 229 | }); 230 | } 231 | 232 | /// 切换 `Cargo` 配置文件中正在使用的镜像 233 | pub fn use_registry( 234 | &mut self, 235 | registry_name: &str, 236 | registry_description: Option<&RegistryDescription>, 237 | remaining_registries: Vec<(&str, &str)>, 238 | ) -> Result<(), String> { 239 | if registry_description.is_none() { 240 | return Err(registry_name.to_string()); 241 | } 242 | 243 | // 获取老的镜像名 244 | let (old_name, _) = self.current(); 245 | 246 | // 替换镜像源 247 | self.replace_with(registry_name); 248 | 249 | // 删除老的镜像属性 250 | self.remove_old_registry(&old_name); 251 | self.remove_old_registries(&[(registry_name, "")]); 252 | self.remove_old_registries(&remaining_registries); 253 | self.append_registries(&remaining_registries); 254 | 255 | if registry_name.eq(RUST_LANG) { 256 | return Ok(()); 257 | } 258 | 259 | // 追加新的镜像属性 260 | self.append_registry( 261 | registry_name, 262 | registry_description.unwrap().registry.to_string(), 263 | ); 264 | 265 | Ok(()) 266 | } 267 | } 268 | 269 | impl Default for CargoConfig { 270 | fn default() -> Self { 271 | Self::new() 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! # 工具模块 2 | //! 3 | //! 工具模块中包含一些简单的函数。 4 | 5 | #![allow(deprecated)] 6 | 7 | use std::{ 8 | env, 9 | ffi::OsStr, 10 | fmt::Display, 11 | fs::{read_to_string, rename}, 12 | io, 13 | path::{Path, PathBuf}, 14 | process::{self, Command, Output, Stdio}, 15 | sync::mpsc, 16 | thread, 17 | time::{Duration, SystemTime}, 18 | }; 19 | 20 | use ureq::Error; 21 | 22 | use crate::constants::{CARGO_HOME, CONFIG, CONFIG_TOML, DOT_CARGO, UNC_PREFIX}; 23 | 24 | pub fn home_dir() -> PathBuf { 25 | env::home_dir().unwrap() 26 | } 27 | 28 | pub fn cargo_home() -> PathBuf { 29 | match env::var_os(CARGO_HOME) { 30 | Some(value) => PathBuf::from(value), 31 | None => home_dir().join(DOT_CARGO), 32 | } 33 | } 34 | 35 | pub fn cargo_config_path() -> PathBuf { 36 | let c = cargo_home(); 37 | 38 | // Rust v1.39 版本中添加了对该 `.toml` 扩展的支持,并且是首选形式 39 | let path = c.join(CONFIG_TOML); 40 | 41 | // Cargo 还读取不带 `.toml` 扩展名的配置文件,例如 `~/.cargo/config` 42 | // 如果该文件存在,Cargo 将首先使用不带扩展名的文件 43 | // https://doc.rust-lang.org/cargo/reference/config.html 44 | let obsolete_path = c.join(CONFIG); 45 | 46 | if path.is_file() { 47 | if obsolete_path.is_file() { 48 | to_out(format!("检测到了两种形式的配置文件,为了避免歧义,请将 {} 文件 (不再被推荐使用) 中的内容手动合并到 {} 文件中", obsolete_path.display(), path.display())); 49 | process::exit(14); 50 | } 51 | } else if obsolete_path.is_file() { 52 | to_out(format!( 53 | "检测到了 {} 配置文件 (不再被推荐使用),以后请使用 {} 配置文件", 54 | obsolete_path.display(), 55 | path.display() 56 | )); 57 | rename(obsolete_path, &path).unwrap(); 58 | } 59 | 60 | path 61 | } 62 | 63 | pub fn get_cargo_config() -> String { 64 | match read_to_string(cargo_config_path()) { 65 | Ok(content) => content, 66 | Err(_) => "".to_string(), 67 | } 68 | } 69 | 70 | pub fn is_registry_name(name: Option<&String>) -> &str { 71 | if name.is_none() { 72 | to_out("请输入正确的镜像名"); 73 | process::exit(1); 74 | } 75 | 76 | name.unwrap().as_str() 77 | } 78 | 79 | pub fn is_registry_addr(addr: Option<&String>) -> &str { 80 | if addr.is_none() { 81 | to_out("请输入正确的镜像地址"); 82 | process::exit(2); 83 | } 84 | 85 | addr.unwrap().as_str() 86 | } 87 | 88 | pub fn is_registry_dl(dl: Option<&String>) -> &str { 89 | if dl.is_none() { 90 | to_out("请输入正确的 dl。\n 每一个镜像源都是一个 Git 存储库,而在该存储库的根目录下有一个 config.json 文件,\n 其中,dl 属性是 config.json 文件中的一个字段。"); 91 | process::exit(3); 92 | } 93 | 94 | dl.unwrap().as_str() 95 | } 96 | 97 | pub fn append_end_spaces(value: &str, total_len: Option) -> String { 98 | let size = if total_len.is_none() { 99 | 15 100 | } else { 101 | Option::unwrap(total_len) 102 | }; 103 | 104 | let pad = if value.len() < size { 105 | " ".repeat(size - value.len()) 106 | } else { 107 | "".to_string() 108 | }; 109 | 110 | format!("{}{}", value, pad) 111 | } 112 | 113 | pub fn request(url: &str, is_connect_only: bool) -> Option { 114 | let time = SystemTime::now(); 115 | 116 | match ureq::get(url).timeout(Duration::from_secs(5)).call() { 117 | Ok(res) => { 118 | let status = res.status(); 119 | 120 | if status >= 300 { 121 | return match res.header("location") { 122 | Some(v) => request(v, is_connect_only), 123 | None => None, 124 | }; 125 | } 126 | 127 | // 不管是不是 404,只要能连上主机,就成功返回 128 | if is_connect_only { 129 | return Some(time.elapsed().unwrap().as_millis()); 130 | } 131 | 132 | if status >= 400 { 133 | return None; 134 | } 135 | 136 | Some(time.elapsed().unwrap().as_millis()) 137 | } 138 | 139 | // 连接成功,但返回的状态不是预期的 140 | Err(Error::Status(_, _)) => { 141 | if is_connect_only { 142 | Some(time.elapsed().unwrap().as_millis()) 143 | } else { 144 | None 145 | } 146 | } 147 | 148 | // 其他错误,例如连接失败 149 | Err(_) => None, 150 | } 151 | } 152 | 153 | pub fn network_delay( 154 | values: Vec<(String, Option)>, 155 | sender_size: Option, 156 | is_connect_only: bool, 157 | ) -> Vec<(String, Option)> { 158 | let (tx, rx) = mpsc::channel(); 159 | let iter = values.iter(); 160 | let len = sender_size.unwrap_or_else(|| iter.len()); 161 | let mut ret = vec![]; 162 | 163 | for v in iter { 164 | let t = tx.clone(); 165 | let v = v.clone(); 166 | 167 | thread::spawn(move || { 168 | let date = match &v.1 { 169 | Some(url) => request(url, is_connect_only), 170 | None => None, 171 | }; 172 | 173 | if t.send((v.0.to_string(), date)).is_err() { 174 | process::exit(0); 175 | } 176 | }); 177 | } 178 | 179 | for _ in 0..len { 180 | ret.push(rx.recv().unwrap()); 181 | } 182 | 183 | ret.sort_by(|a, b| a.1.cmp(&b.1)); 184 | 185 | ret 186 | } 187 | 188 | pub fn field_eprint(field_name: T, field_type: &str) { 189 | to_out(format!( 190 | "{} 文件中的 {} 字段不是一个{},请修改后重试", 191 | cargo_config_path().display(), 192 | field_name, 193 | field_type 194 | )); 195 | } 196 | 197 | pub fn to_out(message: T) { 198 | println!(" {}", message); 199 | } 200 | 201 | pub fn status_prefix(value1: &String, value2: &String) -> String { 202 | if value1.eq(value2) { " * " } else { " " }.to_string() 203 | } 204 | 205 | pub fn is_windows() -> bool { 206 | cfg!(target_os = "windows") 207 | } 208 | 209 | pub fn absolute_path>(dir: &T) -> io::Result { 210 | let mut path = Path::new(dir).canonicalize()?; 211 | let path_str = path.to_str().unwrap(); 212 | 213 | // 如果是 `Windows`,并且当前路径是 `UNC` 路径 214 | if is_windows() && path_str.starts_with(UNC_PREFIX) { 215 | let path_slice = &path_str[UNC_PREFIX.len()..]; 216 | 217 | // 路径不能超过普通 `Windows` 路径的长度 218 | if path_slice.len() > 260 { 219 | let error = io::Error::new( 220 | io::ErrorKind::InvalidData, 221 | format!("当前路径超过了 Windows 普通路径的最大长度: {}", path_str), 222 | ); 223 | 224 | return Err(error); 225 | } 226 | 227 | path = PathBuf::from(path_slice); 228 | } 229 | 230 | Ok(path) 231 | } 232 | 233 | pub fn exec_command(command: &str, cwd: Option<&String>) -> io::Result { 234 | let mut program = "sh"; 235 | let mut arg_c = "-c"; 236 | 237 | if is_windows() { 238 | program = "cmd"; 239 | arg_c = "/c"; 240 | } 241 | 242 | let cwd = match cwd { 243 | Some(cwd) => match absolute_path(cwd) { 244 | Ok(path) => path, 245 | Err(e) => { 246 | return Err(e); 247 | } 248 | }, 249 | None => env::current_dir().unwrap(), 250 | }; 251 | 252 | Command::new(program) 253 | .current_dir(cwd) 254 | .args([arg_c, command]) 255 | .stdin(Stdio::inherit()) 256 | .stdout(Stdio::inherit()) 257 | .stderr(Stdio::inherit()) 258 | .output() 259 | } 260 | 261 | /// 获取最新的版本 262 | pub fn get_newest_version() -> Option { 263 | let url = "https://crates.io/api/v1/crates/crm"; 264 | 265 | match ureq::get(url).timeout(Duration::from_secs(10)).call() { 266 | Ok(res) => { 267 | let status = res.status(); 268 | 269 | if status >= 400 { 270 | return None; 271 | } 272 | 273 | match res.into_string() { 274 | Ok(body) => match body.find("\"newest_version\"") { 275 | Some(idx) => { 276 | let sub_str = &body[idx + 18..idx + 38]; 277 | let version = &sub_str[..sub_str.find('\"').unwrap()]; 278 | 279 | Some(version.to_string()) 280 | } 281 | 282 | None => None, 283 | }, 284 | 285 | Err(_) => None, 286 | } 287 | } 288 | 289 | Err(_) => None, 290 | } 291 | } 292 | 293 | pub fn not_command(command: &str) { 294 | let r = r#" 295 | crm best 评估网络延迟并自动切换到最优的镜像 296 | crm best git 仅评估 git 镜像源 297 | crm best sparse 仅评估支持 sparse 协议的镜像源 298 | crm best git-download 仅评估能够快速下载软件包的 git 镜像源 (推荐使用) 299 | crm best sparse-download 仅评估能够快速下载软件包且支持 sparse 协议的镜像源 (推荐使用) 300 | crm current 获取当前所使用的镜像 301 | crm default 恢复为官方默认镜像 302 | crm install [args] 使用官方镜像执行 "cargo install" 303 | crm list 从镜像配置文件中获取镜像列表 304 | crm publish [args] 使用官方镜像执行 "cargo publish" 305 | crm remove 在镜像配置文件中删除镜像 306 | crm save
在镜像配置文件中添加/更新镜像 307 | crm test [name] 下载测试包以评估网络延迟 308 | crm update [args] 使用官方镜像执行 "cargo update" 309 | crm use 切换为要使用的镜像 310 | crm version 查看当前版本 311 | crm check-update 检测版本更新 312 | "#; 313 | 314 | to_out(format!( 315 | "{} 命令无效。参考:\n{}\nHome:\n", 316 | command, r 317 | )); 318 | process::exit(4); 319 | } 320 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | //! # 对镜像进行操作 2 | //! 3 | //! 该模块用于操作镜像。包括简单的增删改查操作。 4 | 5 | use std::{collections::HashSet, process}; 6 | 7 | use crate::{ 8 | cargo::CargoConfig, 9 | constants::{APP_NAME, APP_VERSION, CARGO, RUST_LANG}, 10 | runtime::RuntimeConfig, 11 | utils::{ 12 | append_end_spaces, exec_command, is_registry_addr, is_registry_dl, is_registry_name, 13 | network_delay, status_prefix, to_out, 14 | }, 15 | }; 16 | 17 | /// 镜像对象 18 | pub struct Registry { 19 | /// 运行时配置 20 | rc: RuntimeConfig, 21 | 22 | /// `Cargo` 配置 23 | cargo: CargoConfig, 24 | } 25 | 26 | impl Registry { 27 | /// 创建镜像对象 28 | pub fn new() -> Self { 29 | Registry { 30 | rc: RuntimeConfig::new(), 31 | cargo: CargoConfig::new(), 32 | } 33 | } 34 | 35 | /// 切换镜像 36 | pub fn select(&mut self, name: Option<&String>) { 37 | let name = is_registry_name(name).trim(); 38 | 39 | // 收集需要添加 `[registries.xxx]` 属性的镜像元祖数组 40 | let remaining_registries = self.rc.to_tuples(None); 41 | 42 | if let Err(name) = self 43 | .cargo 44 | .use_registry(name, self.rc.get(name), remaining_registries) 45 | { 46 | let keys = self.rc.to_key_string(); 47 | 48 | if keys.is_empty() { 49 | return to_out(format!( 50 | "没有找到 {} 镜像,配置中的镜像列表为空,请用 \"crm save\" 添加镜像后重试", 51 | name, 52 | )); 53 | } 54 | 55 | to_out(format!("没有找到 {} 镜像,可选的镜像是:\n{}", name, keys)); 56 | }; 57 | 58 | self.cargo.make(); 59 | } 60 | 61 | /// 删除镜像 62 | pub fn remove(&mut self, name: Option<&String>) { 63 | let name = is_registry_name(name).trim(); 64 | 65 | if self.rc.get_default(name).is_some() { 66 | to_out("请不要删除内置镜像"); 67 | process::exit(7); 68 | } 69 | 70 | if self.rc.get_extend(name).is_none() { 71 | to_out(format!("删除失败,{} 镜像不存在", name)); 72 | process::exit(8); 73 | } 74 | 75 | self.rc.remove(name); 76 | self.rc.write(); 77 | } 78 | 79 | /// 添加/更新镜像 80 | pub fn save(&mut self, name: Option<&String>, addr: Option<&String>, dl: Option<&String>) { 81 | let name = is_registry_name(name).trim(); 82 | let addr = is_registry_addr(addr).trim(); 83 | let dl = is_registry_dl(dl).trim(); 84 | 85 | self.rc.save(name, addr, dl); 86 | self.rc.write(); 87 | } 88 | 89 | /// 获取镜像列表 90 | pub fn list(&self, current: &String) -> String { 91 | self.rc.to_string(current, Some("- ")) 92 | } 93 | 94 | /// 获取当前正在使用的镜像 95 | pub fn current(&self) -> (String, Option) { 96 | let (name, addr) = CargoConfig::new().current(); 97 | let addr = match addr { 98 | Some(addr) => Some(addr), 99 | None => self.rc.get(&name).map(|addr| addr.registry.clone()), 100 | }; 101 | 102 | (name, addr) 103 | } 104 | 105 | /// 恢复为默认镜像 106 | pub fn default(&mut self) { 107 | self.select(Some(&RUST_LANG.to_string())); 108 | } 109 | 110 | fn delay_tester<'a, I, P, F>( 111 | &self, 112 | iter: I, 113 | predicate: P, 114 | conversion_url: F, 115 | ) -> Vec<(String, Option)> 116 | where 117 | I: Iterator, 118 | P: FnMut(&&'a String) -> bool, 119 | F: FnMut(&'a String) -> (String, Option), 120 | { 121 | let urls: Vec<(String, Option)> = 122 | iter.filter(predicate).map(conversion_url).collect(); 123 | 124 | network_delay(urls, Some(1), true) 125 | } 126 | 127 | /// 评估网络延迟并自动切换到最优的镜像 128 | pub fn best(&mut self, mode: Option<&String>) { 129 | let names = self.rc.registry_names(); 130 | let iter = names.iter(); 131 | let dld = ["sjtu", "ustc", "rsproxy"].iter().map(ToString::to_string); 132 | const SPARSE: &str = "-sparse"; 133 | 134 | let tested = match mode { 135 | Some(mode) => match mode.to_lowercase().as_str() { 136 | // 测试所有带有 `sparse` 后缀的镜像源 137 | "sparse" => self.delay_tester( 138 | iter, 139 | |name| name.ends_with(SPARSE), 140 | |name| (name.to_string(), self.to_connected_url(name)), 141 | ), 142 | 143 | // 测试所有 git 镜像源 144 | "git" => self.delay_tester( 145 | iter, 146 | |name| !name.ends_with(SPARSE), 147 | |name| (name.to_string(), self.to_download_url(name)), 148 | ), 149 | 150 | // 仅测试能够快速下载包的 git 镜像源 151 | "git-download" => { 152 | let s = dld.collect::>(); 153 | 154 | self.delay_tester( 155 | iter, 156 | |name| s.contains(&**name), 157 | |name| (name.to_string(), self.to_download_url(name)), 158 | ) 159 | } 160 | 161 | // 仅测试所有能够快速下载包的,并且带有 `sparse` 后缀的镜像源 162 | "sparse-download" => { 163 | let s = dld.map(|v| v + SPARSE).collect::>(); 164 | 165 | self.delay_tester( 166 | iter, 167 | |name| s.contains(&**name), 168 | |name| (name.to_string(), self.to_connected_url(name)), 169 | ) 170 | } 171 | 172 | _ => { 173 | to_out("参数错误,您不能使用除 \"sparse\"、\"git\"、\"git-download\" 或 \"sparse-download\" 之外的值"); 174 | process::exit(9) 175 | } 176 | }, 177 | 178 | None => self.test_download_status(None, Some(1)), 179 | }; 180 | 181 | let found = tested.iter().find(|v| v.1.is_some()); 182 | 183 | if found.is_none() { 184 | return to_out("没有可切换的镜像源"); 185 | } 186 | 187 | let registry_name = &found.unwrap().0; 188 | 189 | self.select(Some(registry_name)); 190 | to_out(format!("已切换到 {} 镜像源", registry_name)); 191 | } 192 | 193 | /// 将 `dl` 转换为 `url` 194 | fn to_download_url(&self, name: &str) -> Option { 195 | match self.rc.get(name) { 196 | Some(rd) => { 197 | let dl = rd.dl.clone(); 198 | let url = if !dl.ends_with("/api/v1/crates") { 199 | dl.replace("{crate}", APP_NAME) 200 | .replace("{version}", APP_VERSION) 201 | } else { 202 | format!("{}/{}/{}/download", dl, APP_NAME, APP_VERSION) 203 | }; 204 | 205 | Some(url) 206 | } 207 | None => None, 208 | } 209 | } 210 | 211 | /// 测试镜像源状态 212 | /// NOTE 仅 `git` 镜像源支持下载状态测试 213 | fn test_download_status( 214 | &self, 215 | name: Option<&String>, 216 | sender_size: Option, 217 | ) -> Vec<(String, Option)> { 218 | let urls = match name { 219 | Some(name) => { 220 | if self.rc.get(name).is_none() { 221 | to_out(format!("下载测试失败,{} 镜像不存在", name)); 222 | process::exit(10); 223 | } 224 | 225 | vec![(name.to_string(), self.to_download_url(name))] 226 | } 227 | None => self 228 | .rc 229 | .registry_names() 230 | .iter() 231 | .filter(|name| !name.ends_with("-sparse")) 232 | .map(|name| (name.to_string(), self.to_download_url(name))) 233 | .collect(), 234 | }; 235 | 236 | network_delay(urls, sender_size, false) 237 | } 238 | 239 | /// 将 `dl` 转换为 `url` 240 | fn to_connected_url(&self, name: &str) -> Option { 241 | match self.rc.get(name) { 242 | Some(rd) => { 243 | let registry = rd.registry.clone(); 244 | 245 | if registry.starts_with("git:") { 246 | return None; 247 | } 248 | 249 | Some(registry.replace("sparse+", "")) 250 | } 251 | None => None, 252 | } 253 | } 254 | 255 | fn test_connected_status( 256 | &self, 257 | name: Option<&String>, 258 | sender_size: Option, 259 | ) -> Vec<(String, Option)> { 260 | let urls = match name { 261 | Some(name) => { 262 | if self.rc.get(name).is_none() { 263 | to_out(format!("连接测试失败,{} 镜像不存在", name)); 264 | process::exit(11); 265 | } 266 | 267 | vec![(name.to_string(), self.to_connected_url(name))] 268 | } 269 | None => self 270 | .rc 271 | .registry_names() 272 | .iter() 273 | .map(|name| (name.to_string(), self.to_connected_url(name))) 274 | .collect(), 275 | }; 276 | 277 | network_delay(urls, sender_size, true) 278 | } 279 | 280 | /// 测试镜像源延迟 281 | pub fn test(&self, current: &String, name: Option<&String>) { 282 | let connected_status: Vec = self 283 | .test_connected_status(name, None) 284 | .iter() 285 | .map(|(name, status)| { 286 | let prefix = status_prefix(name, current); 287 | let name = append_end_spaces(name, None); 288 | let status = match status { 289 | Some(s) => format!("{} ms", s), 290 | None => "failed".to_string(), 291 | }; 292 | 293 | format!("{}{} -- {}", prefix, name, status) 294 | }) 295 | .collect(); 296 | 297 | println!("网络连接延迟:\n{}\n", connected_status.join("\n")); 298 | 299 | let download_status: Vec = self 300 | .test_download_status(name, None) 301 | .iter() 302 | .map(|(name, status)| { 303 | let prefix = status_prefix(name, current); 304 | let name = append_end_spaces(name, None); 305 | let status = match status { 306 | Some(s) => format!("{} ms", s), 307 | None => "failed".to_string(), 308 | }; 309 | 310 | format!("{}{} -- {}", prefix, name, status) 311 | }) 312 | .collect(); 313 | 314 | println!("软件包下载延迟:\n{}", download_status.join("\n")); 315 | } 316 | 317 | /// 使用官方镜像源执行命令 318 | fn exec(&mut self, command: &str) { 319 | let (registry_name, _) = self.current(); 320 | let is_default_registry = registry_name.eq(RUST_LANG); 321 | 322 | if !is_default_registry { 323 | self.default(); 324 | } 325 | 326 | if let Err(e) = exec_command(command, None) { 327 | to_out(e); 328 | }; 329 | 330 | if !is_default_registry { 331 | self.select(Some(®istry_name)); 332 | } 333 | } 334 | 335 | /// 使用官方镜像执行 `cargo publish` 336 | pub fn publish(&mut self, args: String) { 337 | self.exec(&format!("{} publish {}", CARGO, args.trim())); 338 | } 339 | 340 | /// 使用官方镜像执行 `cargo update` 341 | pub fn update(&mut self, args: String) { 342 | self.exec(&format!("{} update {}", CARGO, args.trim())); 343 | } 344 | 345 | /// 使用官方镜像执行 `cargo install` 346 | pub fn install(&mut self, args: String) { 347 | let args = args.trim(); 348 | let args = if args.is_empty() { "--help" } else { args }; 349 | 350 | self.exec(&format!("{} install {}", CARGO, args)); 351 | } 352 | } 353 | 354 | impl Default for Registry { 355 | fn default() -> Self { 356 | Self::new() 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /LICENSE-Apache-2.0: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 wtklbm 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "inputs": [ 4 | { 5 | "id": "entry", 6 | "type": "promptString", 7 | "description": "文件路径或文件夹路径" 8 | }, 9 | { 10 | "id": "crate", 11 | "type": "promptString", 12 | "description": "crate 名" 13 | }, 14 | { 15 | "id": "example", 16 | "type": "promptString", 17 | "description": "示例名" 18 | }, 19 | { 20 | "id": "test", 21 | "type": "promptString", 22 | "description": "测试名" 23 | }, 24 | { 25 | "id": "bench", 26 | "type": "promptString", 27 | "description": "基准测试名" 28 | }, 29 | { 30 | "id": "file", 31 | "type": "promptString", 32 | "description": "文件名" 33 | } 34 | ], 35 | "tasks": [ 36 | { 37 | "label": "cargoe: 翻译 Rust 模块", 38 | "type": "shell", 39 | "command": "cargoe", 40 | "args": ["t", "module"], 41 | "problemMatcher": [], 42 | "presentation": { 43 | "focus": true, 44 | "clear": true 45 | } 46 | }, 47 | { 48 | "label": "cmtor: 翻译文件/文件夹", 49 | "type": "shell", 50 | "command": "cmtor", 51 | "args": ["p", "${input:entry}"], 52 | "problemMatcher": [], 53 | "presentation": { 54 | "focus": true, 55 | "clear": true 56 | } 57 | }, 58 | { 59 | "label": "Rust: 运行", 60 | "type": "cargo", 61 | "command": "run", 62 | "args": ["-v", "--all-features"], 63 | "problemMatcher": ["$rustc"], 64 | "presentation": { 65 | "focus": true, 66 | "clear": true 67 | } 68 | }, 69 | { 70 | "label": "Rust: Nightly 运行", 71 | "type": "cargo", 72 | "command": "+nightly", 73 | "args": ["run", "-v", "--all-features"], 74 | "problemMatcher": ["$rustc"], 75 | "presentation": { 76 | "focus": true, 77 | "clear": true 78 | } 79 | }, 80 | { 81 | "label": "Rust: 运行指定的 crate", 82 | "type": "cargo", 83 | "command": "run", 84 | "args": ["-v", "-p", "${input:crate}", "--all-features"], 85 | "problemMatcher": ["$rustc"], 86 | "presentation": { 87 | "focus": true, 88 | "clear": true 89 | } 90 | }, 91 | { 92 | "label": "Rust: Nightly 运行指定的 crate", 93 | "type": "cargo", 94 | "command": "+nightly", 95 | "args": ["run", "-v", "-p", "${input:crate}", "--all-features"], 96 | "problemMatcher": ["$rustc"], 97 | "presentation": { 98 | "focus": true, 99 | "clear": true 100 | } 101 | }, 102 | { 103 | "label": "Rust: Clippy", 104 | "type": "cargo", 105 | "command": "clippy", 106 | "args": ["-v", "--all-features"], 107 | "problemMatcher": ["$rustc"], 108 | "presentation": { 109 | "reveal": "silent", 110 | "focus": true, 111 | "clear": true 112 | } 113 | }, 114 | { 115 | "label": "Rust: Nightly Clippy", 116 | "type": "cargo", 117 | "command": "+nightly", 118 | "args": ["clippy", "-v", "--all-features"], 119 | "problemMatcher": ["$rustc"], 120 | "presentation": { 121 | "reveal": "silent", 122 | "focus": true, 123 | "clear": true 124 | } 125 | }, 126 | { 127 | "label": "Rust: 构建开发版本", 128 | "type": "cargo", 129 | "command": "build", 130 | "args": ["-v"], 131 | "problemMatcher": ["$rustc"], 132 | "group": "build", 133 | "presentation": { 134 | "reveal": "silent", 135 | "focus": true, 136 | "clear": true 137 | } 138 | }, 139 | { 140 | "label": "Rust: Nightly 构建开发版本", 141 | "type": "cargo", 142 | "command": "+nightly", 143 | "args": ["build", "-v"], 144 | "problemMatcher": ["$rustc"], 145 | "group": "build", 146 | "presentation": { 147 | "reveal": "silent", 148 | "focus": true, 149 | "clear": true 150 | } 151 | }, 152 | { 153 | "label": "Rust: 构建生产版本", 154 | "type": "cargo", 155 | "command": "build", 156 | "args": ["-v", "--release"], 157 | "problemMatcher": ["$rustc"], 158 | "group": "build", 159 | "presentation": { 160 | "focus": true, 161 | "clear": true 162 | } 163 | }, 164 | { 165 | "label": "Rust: Nightly 构建生产版本", 166 | "type": "cargo", 167 | "command": "+nightly", 168 | "args": ["build", "-v", "--release", "-Ztimings"], 169 | "problemMatcher": ["$rustc"], 170 | "group": "build", 171 | "presentation": { 172 | "focus": true, 173 | "clear": true 174 | } 175 | }, 176 | { 177 | "label": "Rust: 构建生产版本 (特定于当前 CPU)", 178 | "type": "cargo", 179 | "command": "build", 180 | "args": ["-v", "--release"], 181 | "problemMatcher": ["$rustc"], 182 | "group": "build", 183 | "presentation": { 184 | "focus": true, 185 | "clear": true 186 | } 187 | }, 188 | { 189 | "label": "Rust: Nightly 构建生产版本 (特定于当前 CPU)", 190 | "type": "cargo", 191 | "command": "+nightly", 192 | "args": ["build", "-v", "--release", "-Ztimings"], 193 | "problemMatcher": ["$rustc"], 194 | "group": "build", 195 | "presentation": { 196 | "focus": true, 197 | "clear": true 198 | } 199 | }, 200 | { 201 | "label": "Rust: 检查", 202 | "type": "cargo", 203 | "command": "check", 204 | "args": ["-v"], 205 | "problemMatcher": ["$rustc"], 206 | "presentation": { 207 | "reveal": "silent", 208 | "focus": true, 209 | "clear": true 210 | } 211 | }, 212 | { 213 | "label": "Rust: Nightly 检查", 214 | "type": "cargo", 215 | "command": "+nightly", 216 | "args": ["check", "-v"], 217 | "problemMatcher": ["$rustc"], 218 | "presentation": { 219 | "reveal": "silent", 220 | "focus": true, 221 | "clear": true 222 | } 223 | }, 224 | { 225 | "label": "Rust: 清理", 226 | "type": "cargo", 227 | "command": "clean", 228 | "args": ["-v"], 229 | "problemMatcher": ["$rustc"], 230 | "presentation": { 231 | "reveal": "silent", 232 | "focus": true, 233 | "clear": true 234 | } 235 | }, 236 | { 237 | "label": "Rust: Nightly 清理", 238 | "type": "cargo", 239 | "command": "+nightly", 240 | "args": ["clean", "-v"], 241 | "problemMatcher": ["$rustc"], 242 | "presentation": { 243 | "reveal": "silent", 244 | "focus": true, 245 | "clear": true 246 | } 247 | }, 248 | { 249 | "label": "Rust: 文档", 250 | "type": "cargo", 251 | "command": "doc", 252 | "args": [ 253 | "-v", 254 | "--workspace", 255 | "--document-private-items", 256 | "--all-features", 257 | "--open" 258 | ], 259 | "problemMatcher": ["$rustc"], 260 | "presentation": { 261 | "reveal": "silent", 262 | "focus": false, 263 | "clear": true 264 | } 265 | }, 266 | { 267 | "label": "Rust: Nightly 文档", 268 | "type": "cargo", 269 | "command": "+nightly", 270 | "args": [ 271 | "doc", 272 | "-v", 273 | "--workspace", 274 | "--document-private-items", 275 | "--all-features", 276 | "--open" 277 | ], 278 | "problemMatcher": ["$rustc"], 279 | "presentation": { 280 | "reveal": "silent", 281 | "focus": false, 282 | "clear": true 283 | } 284 | }, 285 | { 286 | "label": "Rust: 测试", 287 | "type": "cargo", 288 | "command": "test", 289 | "args": ["-v", "--", "--nocapture"], 290 | "problemMatcher": ["$rustc"], 291 | "group": "test", 292 | "presentation": { 293 | "focus": true, 294 | "clear": true 295 | } 296 | }, 297 | { 298 | "label": "Rust: Nightly 测试", 299 | "type": "cargo", 300 | "command": "+nightly", 301 | "args": ["test", "-v", "--", "--nocapture"], 302 | "problemMatcher": ["$rustc"], 303 | "group": "test", 304 | "presentation": { 305 | "focus": true, 306 | "clear": true 307 | } 308 | }, 309 | { 310 | "label": "Rust: 测试工作空间", 311 | "type": "cargo", 312 | "command": "test", 313 | "args": ["-v", "--workspace", "--", "--nocapture"], 314 | "problemMatcher": ["$rustc"], 315 | "group": "test", 316 | "presentation": { 317 | "focus": true, 318 | "clear": true 319 | } 320 | }, 321 | { 322 | "label": "Rust: 测试指定的测试", 323 | "type": "cargo", 324 | "command": "test", 325 | "args": ["${input:test}", "-v", "--", "--nocapture"], 326 | "problemMatcher": ["$rustc"], 327 | "group": "test", 328 | "presentation": { 329 | "focus": true, 330 | "clear": true 331 | } 332 | }, 333 | { 334 | "label": "Rust: 测试被忽略的", 335 | "type": "cargo", 336 | "command": "test", 337 | "args": ["-v", "--", "--ignored", "--nocapture"], 338 | "problemMatcher": ["$rustc"], 339 | "group": "test", 340 | "presentation": { 341 | "focus": true, 342 | "clear": true 343 | } 344 | }, 345 | { 346 | "label": "Rust: 测试库 crate", 347 | "type": "cargo", 348 | "command": "test", 349 | "args": ["-v", "--lib", "--", "--nocapture"], 350 | "problemMatcher": ["$rustc"], 351 | "group": "test", 352 | "presentation": { 353 | "focus": true, 354 | "clear": true 355 | } 356 | }, 357 | { 358 | "label": "Rust: 测试二进制 crate", 359 | "type": "cargo", 360 | "command": "test", 361 | "args": ["-v", "--bins", "--", "--nocapture"], 362 | "problemMatcher": ["$rustc"], 363 | "group": "test", 364 | "presentation": { 365 | "focus": true, 366 | "clear": true 367 | } 368 | }, 369 | { 370 | "label": "Rust: 测试指定的二进制 crate", 371 | "type": "cargo", 372 | "command": "test", 373 | "args": ["-v", "--bin", "${input:crate}", "--", "--nocapture"], 374 | "problemMatcher": ["$rustc"], 375 | "group": "test", 376 | "presentation": { 377 | "focus": true, 378 | "clear": true 379 | } 380 | }, 381 | { 382 | "label": "Rust: 单线程测试", 383 | "type": "cargo", 384 | "command": "test", 385 | "args": ["-v", "--", "--nocapture", "--test-threads=1"], 386 | "problemMatcher": ["$rustc"], 387 | "group": "test", 388 | "presentation": { 389 | "focus": true, 390 | "clear": true 391 | } 392 | }, 393 | { 394 | "label": "Rust: 测试示例", 395 | "type": "cargo", 396 | "command": "test", 397 | "args": ["-v", "--examples", "--", "--nocapture"], 398 | "problemMatcher": ["$rustc"], 399 | "group": "test", 400 | "presentation": { 401 | "focus": true, 402 | "clear": true 403 | } 404 | }, 405 | { 406 | "label": "Rust: 测试指定的示例", 407 | "type": "cargo", 408 | "command": "test", 409 | "args": [ 410 | "-v", 411 | "--example", 412 | "${input:example}", 413 | "--", 414 | "--nocapture" 415 | ], 416 | "problemMatcher": ["$rustc"], 417 | "group": "test", 418 | "presentation": { 419 | "focus": true, 420 | "clear": true 421 | } 422 | }, 423 | { 424 | "label": "Rust: 测试文档", 425 | "type": "cargo", 426 | "command": "test", 427 | "args": ["-v", "--doc", "--", "--nocapture"], 428 | "problemMatcher": ["$rustc"], 429 | "group": "test", 430 | "presentation": { 431 | "focus": true, 432 | "clear": true 433 | } 434 | }, 435 | { 436 | "label": "Rust: 基准测试", 437 | "type": "cargo", 438 | "command": "test", 439 | "args": ["-v", "--benches", "--", "--nocapture"], 440 | "problemMatcher": ["$rustc"], 441 | "group": "test", 442 | "presentation": { 443 | "focus": true, 444 | "clear": true 445 | } 446 | }, 447 | { 448 | "label": "Rust: 测试指定的基准测试", 449 | "type": "cargo", 450 | "command": "test", 451 | "args": ["-v", "--bench", "${input:bench}", "--", "--nocapture"], 452 | "problemMatcher": ["$rustc"], 453 | "group": "test", 454 | "presentation": { 455 | "focus": true, 456 | "clear": true 457 | } 458 | }, 459 | { 460 | "label": "Rust: 集成测试", 461 | "type": "cargo", 462 | "command": "test", 463 | "args": ["-v", "--tests", "--", "--nocapture"], 464 | "problemMatcher": ["$rustc"], 465 | "group": "test", 466 | "presentation": { 467 | "focus": true, 468 | "clear": true 469 | } 470 | }, 471 | { 472 | "label": "Rust: 测试指定的集成测试", 473 | "type": "cargo", 474 | "command": "test", 475 | "args": ["-v", "--test", "${input:file}", "--", "--nocapture"], 476 | "problemMatcher": ["$rustc"], 477 | "group": "test", 478 | "presentation": { 479 | "focus": true, 480 | "clear": true 481 | } 482 | }, 483 | { 484 | "label": "Rust: Lib llvm-lines", 485 | "type": "cargo", 486 | "command": "llvm-lines", 487 | "args": ["--lib"], 488 | "problemMatcher": ["$rustc"], 489 | "presentation": { 490 | "focus": true, 491 | "clear": true 492 | } 493 | }, 494 | { 495 | "label": "Rust: Bin llvm-lines", 496 | "type": "cargo", 497 | "command": "llvm-lines", 498 | "args": ["--bin", "${input:crate}"], 499 | "problemMatcher": ["$rustc"], 500 | "presentation": { 501 | "focus": true, 502 | "clear": true 503 | } 504 | } 505 | ] 506 | } 507 | --------------------------------------------------------------------------------