diff --git a/Cargo.lock b/Cargo.lock index 7cea196..ac9a089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,7 +455,7 @@ dependencies = [ [[package]] name = "dadk" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "clap", @@ -478,13 +478,14 @@ dependencies = [ [[package]] name = "dadk-config" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "cfg-if", "env_logger", "indexmap", "log", + "regex", "serde", "serde_json", "shlex", @@ -495,7 +496,7 @@ dependencies = [ [[package]] name = "dadk-user" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "chrono", diff --git a/dadk-config/Cargo.toml b/dadk-config/Cargo.toml index 09ced40..f3f7557 100644 --- a/dadk-config/Cargo.toml +++ b/dadk-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dadk-config" -version = "0.4.0" +version = "0.5.0" edition = "2021" authors = [ "longjin ", @@ -20,6 +20,7 @@ serde = { workspace = true } serde_json = { workspace = true } shlex = "1.3.0" toml = "0.8.12" +regex = "1.10" # 只有在test的情况下才会引入下列库 [dev-dependencies] diff --git a/dadk-config/src/app_blocklist.rs b/dadk-config/src/app_blocklist.rs new file mode 100644 index 0000000..8dd0c66 --- /dev/null +++ b/dadk-config/src/app_blocklist.rs @@ -0,0 +1,434 @@ +use anyhow::Result; +use serde::Deserialize; +use std::collections::HashSet; +use std::path::PathBuf; + +/// 被屏蔽的应用程序信息 +#[derive(Debug, Clone, Deserialize)] +pub struct BlockedApp { + /// 应用名称或模式 + pub name: String, + /// 屏蔽原因(可选) + #[serde(default)] + pub reason: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AppBlocklistConfigFile { + /// 被屏蔽的应用程序列表,每个应用可独立设置reason + #[serde(default)] + pub blocked_apps: Vec, + + /// 是否启用严格模式 + #[serde(default = "default_strict_mode")] + pub strict: bool, + + /// 是否记录被跳过的应用 + #[serde(default = "default_log_skipped")] + pub log_skipped: bool, + + #[serde(skip)] + app_patterns: HashSet, +} + +impl AppBlocklistConfigFile { + /// 从文件加载应用黑名单配置 + /// + /// # Arguments + /// * `path` - 配置文件路径 + /// + /// # Returns + /// * `Result` - 解析后的配置或错误 + /// + /// # Notes + /// 如果文件不存在,返回一个空的默认配置 + pub fn load(path: &PathBuf) -> Result { + if !path.exists() { + // 文件不存在时返回空配置 + return Ok(Self { + blocked_apps: Vec::new(), + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }); + } + + let content = std::fs::read_to_string(path)?; + let mut config = Self::load_from_str(&content)?; + + // 预处理模式匹配 + config.app_patterns = config + .blocked_apps + .iter() + .map(|app| app.name.clone()) + .collect(); + + Ok(config) + } + + /// 从字符串内容加载应用黑名单配置 + /// + /// # Arguments + /// * `content` - TOML 格式的配置内容 + /// + /// # Returns + /// * `Result` - 解析后的配置或错误 + pub fn load_from_str(content: &str) -> Result { + let config: AppBlocklistConfigFile = toml::from_str(content)?; + Ok(config) + } + + /// 检查应用是否被屏蔽 + /// + /// # Arguments + /// * `app_name` - 应用名称 + /// * `version` - 可选的版本号 + /// + /// # Returns + /// * `true` - 如果应用被屏蔽 + /// * `false` - 如果应用未被屏蔽 + pub fn is_blocked(&self, app_name: &str, version: Option<&str>) -> bool { + // 1. 精确匹配(无版本) + if self.blocked_apps.iter().any(|app| app.name == app_name) { + return true; + } + + // 2. 带版本的精确匹配 + if let Some(version) = version { + let versioned_name = format!("{}@{}", app_name, version); + if self + .blocked_apps + .iter() + .any(|app| app.name == versioned_name) + { + return true; + } + } + + // 3. 模式匹配(支持通配符) + for app in &self.blocked_apps { + if self.match_pattern(app_name, version, &app.name) { + return true; + } + } + + false + } + + /// 模式匹配(支持通配符和版本匹配) + /// + /// # Arguments + /// * `name` - 应用名称 + /// * `version` - 可选的版本号 + /// * `pattern` - 匹配模式,支持以下格式: + /// - 精确名称:`app1` + /// - 带版本:`app1@1.0.0` + /// - 通配符名称:`test-*`, `test-?` + /// - 通配符版本:`app1@1.*`, `app1@1.?.0` + /// + /// # Returns + /// * `true` - 如果应用名称和版本匹配模式 + /// * `false` - 如果不匹配 + fn match_pattern(&self, name: &str, version: Option<&str>, pattern: &str) -> bool { + // 检查是否包含 @ 符号 + if let Some(at_pos) = pattern.find('@') { + // 分离名称模式和版本模式 + let name_pattern = &pattern[..at_pos]; + let version_pattern = &pattern[at_pos + 1..]; + + // 检查名称是否匹配 + if name_pattern.contains('*') || name_pattern.contains('?') { + let regex_name_pattern = name_pattern + .replace('.', "\\.") + .replace('*', ".*") + .replace('?', "."); + + if let Ok(re) = regex::Regex::new(&format!("^{}$", regex_name_pattern)) { + if !re.is_match(name) { + return false; + } + } + } else if name_pattern != name { + return false; + } + + // 检查版本是否匹配 + if let Some(version) = version { + if version_pattern.contains('*') || version_pattern.contains('?') { + let regex_version_pattern = version_pattern + .replace('.', "\\.") + .replace('*', ".*") + .replace('?', "."); + + if let Ok(re) = regex::Regex::new(&format!("^{}$", regex_version_pattern)) { + return re.is_match(version); + } + } else { + return version_pattern == version; + } + } + + false + } else { + // 没有 @ 符号,只匹配名称 + if pattern.contains('*') || pattern.contains('?') { + let regex_pattern = pattern + .replace('.', "\\.") + .replace('*', ".*") + .replace('?', "."); + + if let Ok(re) = regex::Regex::new(&format!("^{}$", regex_pattern)) { + return re.is_match(name); + } + } + false + } + } + + /// 获取被屏蔽的应用数量 + /// + /// # Returns + /// * `usize` - 黑名单中的应用数量 + pub fn blocked_count(&self) -> usize { + self.blocked_apps.len() + } + + /// 获取应用的屏蔽原因 + /// + /// # Arguments + /// * `app_name` - 应用名称 + /// * `version` - 可选的版本号 + /// + /// # Returns + /// * `Option<&String>` - 如果应用被屏蔽则返回原因,否则返回None + pub fn get_blocked_reason(&self, app_name: &str, version: Option<&str>) -> Option<&String> { + // 1. 精确匹配(无版本) + if let Some(app) = self.blocked_apps.iter().find(|app| app.name == app_name) { + return app.reason.as_ref(); + } + + // 2. 带版本的精确匹配 + if let Some(version) = version { + let versioned_name = format!("{}@{}", app_name, version); + if let Some(app) = self + .blocked_apps + .iter() + .find(|app| app.name == versioned_name) + { + return app.reason.as_ref(); + } + } + + // 3. 模式匹配(支持通配符) + for app in &self.blocked_apps { + if self.match_pattern(app_name, version, &app.name) { + return app.reason.as_ref(); + } + } + + None + } + + /// 获取所有带reason的屏蔽应用 + /// + /// # Returns + /// * `Vec<(&String, &String)>` - 返回(name, reason)对的向量 + pub fn blocked_apps_with_reason(&self) -> Vec<(&String, &String)> { + self.blocked_apps + .iter() + .filter(|app| app.reason.is_some()) + .map(|app| (&app.name, app.reason.as_ref().unwrap())) + .collect() + } + + /// 获取所有被屏蔽的应用名称 + /// + /// # Returns + /// * `Vec<&String>` - 所有被屏蔽应用的名称列表 + pub fn blocked_app_names(&self) -> Vec<&String> { + self.blocked_apps.iter().map(|app| &app.name).collect() + } +} + +fn default_strict_mode() -> bool { + true +} + +fn default_log_skipped() -> bool { + true +} + +impl Default for AppBlocklistConfigFile { + fn default() -> Self { + Self { + blocked_apps: Vec::new(), + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_exact_match() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![ + BlockedApp { + name: "app1".to_string(), + reason: Some("Test reason 1".to_string()), + }, + BlockedApp { + name: "app2".to_string(), + reason: None, + }, + ], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + assert!(config.is_blocked("app1", None)); + assert!(config.is_blocked("app2", None)); + assert!(!config.is_blocked("app3", None)); + + // Test getting reasons + assert_eq!( + config.get_blocked_reason("app1", None), + Some(&"Test reason 1".to_string()) + ); + assert_eq!(config.get_blocked_reason("app2", None), None); + assert_eq!(config.get_blocked_reason("app3", None), None); + } + + #[test] + fn test_pattern_match() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![ + BlockedApp { + name: "test-*".to_string(), + reason: Some("Test applications".to_string()), + }, + BlockedApp { + name: "deprecated-*".to_string(), + reason: Some("Deprecated applications".to_string()), + }, + ], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + assert!(config.is_blocked("test-app", None)); + assert!(config.is_blocked("test-utils", None)); + assert!(config.is_blocked("deprecated-old", None)); + assert!(!config.is_blocked("new-app", None)); + + // Test pattern matching returns reason + assert_eq!( + config.get_blocked_reason("test-app", None), + Some(&"Test applications".to_string()) + ); + assert_eq!( + config.get_blocked_reason("deprecated-old", None), + Some(&"Deprecated applications".to_string()) + ); + } + + #[test] + fn test_empty_blocklist() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + assert!(!config.is_blocked("any-app", None)); + assert_eq!(config.blocked_count(), 0); + } + + #[test] + fn test_versioned_match() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![ + BlockedApp { + name: "openssl@1.1.1".to_string(), + reason: Some("Vulnerable version".to_string()), + }, + BlockedApp { + name: "nginx".to_string(), + reason: None, + }, + ], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + assert!(config.is_blocked("openssl", Some("1.1.1"))); + assert!(!config.is_blocked("openssl", Some("3.0.0"))); + assert!(config.is_blocked("nginx", None)); + + assert_eq!( + config.get_blocked_reason("openssl", Some("1.1.1")), + Some(&"Vulnerable version".to_string()) + ); + assert_eq!(config.get_blocked_reason("nginx", None), None); + } + + #[test] + fn test_blocked_apps_with_reason() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![ + BlockedApp { + name: "app1".to_string(), + reason: Some("Reason 1".to_string()), + }, + BlockedApp { + name: "app2".to_string(), + reason: None, + }, + BlockedApp { + name: "app3".to_string(), + reason: Some("Reason 3".to_string()), + }, + ], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + let with_reason = config.blocked_apps_with_reason(); + assert_eq!(with_reason.len(), 2); + assert!(with_reason.contains(&(&"app1".to_string(), &"Reason 1".to_string()))); + assert!(with_reason.contains(&(&"app3".to_string(), &"Reason 3".to_string()))); + } + + #[test] + fn test_blocked_app_names() { + let config = AppBlocklistConfigFile { + blocked_apps: vec![ + BlockedApp { + name: "app1".to_string(), + reason: Some("Reason 1".to_string()), + }, + BlockedApp { + name: "app2".to_string(), + reason: None, + }, + ], + strict: true, + log_skipped: true, + app_patterns: HashSet::new(), + }; + + let names = config.blocked_app_names(); + assert_eq!(names.len(), 2); + assert!(names.contains(&&"app1".to_string())); + assert!(names.contains(&&"app2".to_string())); + } +} diff --git a/dadk-config/src/lib.rs b/dadk-config/src/lib.rs index 5efbda7..e96a3f4 100644 --- a/dadk-config/src/lib.rs +++ b/dadk-config/src/lib.rs @@ -1,4 +1,5 @@ #![deny(clippy::all)] +pub mod app_blocklist; pub mod boot; pub mod common; pub mod manifest; diff --git a/dadk-config/src/manifest.rs b/dadk-config/src/manifest.rs index 31fa0b9..2b73d7a 100644 --- a/dadk-config/src/manifest.rs +++ b/dadk-config/src/manifest.rs @@ -86,6 +86,13 @@ pub struct Metadata { #[deprecated(note = "This field is deprecated and will be removed in DADK 1.0")] #[serde(default = "default_user_config_dir", rename = "user-config-dir")] pub user_config_dir: PathBuf, + + /// Application blocklist configuration file path + #[serde( + default = "default_app_blocklist_config_path", + rename = "app-blocklist-config" + )] + pub app_blocklist_config: PathBuf, } /// Returns the default path for the rootfs configuration file. @@ -123,6 +130,12 @@ fn default_user_config_dir() -> PathBuf { "user/dadk/config".into() } +/// Returns the default path for the application blocklist configuration file. +fn default_app_blocklist_config_path() -> PathBuf { + set_used_default(); + "config/app-blocklist.toml".into() +} + #[cfg(test)] mod tests { use super::*; @@ -141,6 +154,7 @@ mod tests { sysroot-dir = "bin/sysroot" cache-root-dir = "bin/dadk_cache" user-config-dir = "user/dadk/config" + app-blocklist-config = "config/app-blocklist.toml" "#; let mut temp_file = NamedTempFile::new()?; diff --git a/dadk-config/templates/config/app-blocklist.toml b/dadk-config/templates/config/app-blocklist.toml new file mode 100644 index 0000000..2fb60f9 --- /dev/null +++ b/dadk-config/templates/config/app-blocklist.toml @@ -0,0 +1,153 @@ +# ========================================================= +# DADK 应用程序黑名单配置文件模板 +# 路径: config/app-blocklist.toml +# ========================================================= +# +# 应用程序黑名单功能允许用户指定不希望编译和安装的应用程序。 +# 当黑名单中的应用程序被检测到时,DADK 会根据配置自动跳过这些应用程序的构建和安装过程。 + +# ========================================================= +# 全局配置选项 +# ========================================================= + +# 是否启用严格模式(可选) +# - true(默认):严格模式,跳过被屏蔽的应用程序并记录警告 +# - false:非严格模式,只记录警告但不跳过应用程序 +strict = true + +# 是否在日志中显示被跳过的应用(可选) +# - true(默认):在日志中显示被跳过的应用程序信息 +# - false:静默模式,不显示被跳过的应用程序 +log_skipped = true + +# ========================================================= +# 被屏蔽的应用程序列表 +# ========================================================= +# +# 支持以下匹配方式: +# 1. 精确匹配: name = "app1" +# 2. 版本匹配: name = "openssl@1.1.1" +# 3. 通配符名称: name = "test-*" +# 4. 通配符版本: name = "nginx@1.*" +# 5. 复合模式: name = "lib*@2.*" +# +# 每个应用可以选择性地提供屏蔽原因(reason) + +# ------------------------- +# 精确匹配示例 +# ------------------------- +# 屏蔽特定应用程序 +[[blocked_apps]] +name = "busybox" +reason = "已被musl libc取代,不再需要" + +[[blocked_apps]] +name = "deprecated-app" +reason = "应用程序已弃用" + +# 无reason的应用(reason字段可选) +[[blocked_apps]] +name = "test-app" + +# ------------------------- +# 版本匹配示例 +# ------------------------- +# 屏蔽特定版本的应用程序 +[[blocked_apps]] +name = "openssl@1.1.1" +reason = "存在安全漏洞CVE-2021-3711,请使用3.x版本" + +[[blocked_apps]] +name = "nginx@1.18.0" +reason = "存在已知bug,建议使用1.20.0及以上版本" + +# ------------------------- +# 通配符模式匹配示例 +# ------------------------- +# 屏蔽所有测试应用程序 +[[blocked_apps]] +name = "test-*" +reason = "测试应用程序,生产环境不需要" + +# 屏蔽所有以"old-"开头的应用 +[[blocked_apps]] +name = "old-*" +reason = "旧版本应用程序" + +# 屏蔽所有debug工具 +[[blocked_apps]] +name = "*-debug" +reason = "调试工具,发布版本不需要" + +# ------------------------- +# 版本通配符匹配示例 +# ------------------------- +# 屏蔽所有nginx版本 +[[blocked_apps]] +name = "nginx@*" +reason = "当前项目不需要Web服务器" + +# 屏蔽特定主版本的所有版本 +[[blocked_apps]] +name = "libfoo@2.*" +reason = "2.x版本不兼容当前系统" + +# 屏蔽特定模式的版本 +[[blocked_apps]] +name = "lib*@1.*" +reason = "所有lib开头的1.x版本库都已过时" + +# ------------------------- +# 安全相关屏蔽示例 +# ------------------------- +[[blocked_apps]] +name = "curl@7.58.*" +reason = "存在安全漏洞CVE-2018-16842" + +[[blocked_apps]] +name = "sqlite3@3.31.*" +reason = "存在数据损坏风险" + +# ------------------------- +# 架构相关屏蔽示例 +# ------------------------- +[[blocked_apps]] +name = "x86-*" +reason = "当前构建目标不是x86架构" + +# ------------------------- +# 依赖冲突屏蔽示例 +# ------------------------- +[[blocked_apps]] +name = "conflicting-lib" +reason = "与主要依赖库存在冲突" + +# ========================================================= +# 配置说明和注意事项 +# ========================================================= +# +# 1. **依赖关系**:如果其他应用程序依赖被屏蔽的应用程序,构建过程可能会失败。 +# 请确保处理好依赖关系。 +# +# 2. **模式匹配优先级**: +# - 精确匹配 > 版本匹配 > 模式匹配 +# - 如果有多个模式都匹配,使用第一个匹配的结果 +# +# 3. **通配符语法**: +# - "*" 匹配任意数量的字符(包括0个) +# - "?" 匹配单个字符 +# - 支持在名称和版本中使用通配符 +# +# 4. **版本格式**: +# - 版本号使用 "@" 符号分隔,如 "app@1.0.0" +# - 版本号支持通配符,如 "app@1.*" +# +# 5. **配置文件路径**: +# - 默认路径:config/app-blocklist.toml +# - 可在 dadk-manifest.toml 中通过 app-blocklist-config 字段自定义路径 +# +# 6. **字段说明**: +# - name:应用程序名称或匹配模式(必需) +# - reason:屏蔽原因说明(可选,建议提供以便调试和维护) +# - strict:全局严格模式开关 +# - log_skipped:全局日志记录开关 \ No newline at end of file diff --git a/dadk-config/templates/dadk-manifest.toml b/dadk-config/templates/dadk-manifest.toml index d7df3a6..cd89a6f 100644 --- a/dadk-config/templates/dadk-manifest.toml +++ b/dadk-config/templates/dadk-manifest.toml @@ -23,3 +23,6 @@ cache-root-dir = "bin/dadk_cache" # User configuration directory path # 这个字段只是临时用于兼容旧版本,v0.2版本重构完成后会删除 user-config-dir = "user/apps/dadk/config" + +# Application blocklist configuration file path +app-blocklist-config = "config/app-blocklist.toml" diff --git a/dadk-config/tests/test_app_blocklist.rs b/dadk-config/tests/test_app_blocklist.rs new file mode 100644 index 0000000..7656a07 --- /dev/null +++ b/dadk-config/tests/test_app_blocklist.rs @@ -0,0 +1,186 @@ +//! 测试应用程序黑名单功能 + +use dadk_config::app_blocklist::AppBlocklistConfigFile; +use std::path::PathBuf; + +#[test] +fn test_app_blocklist_exact_match() { + let config_content = r#" + [[blocked_apps]] + name = "app1" + reason = "Test app 1" + + [[blocked_apps]] + name = "app2" + + [[blocked_apps]] + name = "app3" + reason = "Test app 3" + + strict = true + log_skipped = true + "#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + assert!(config.is_blocked("app1", None)); + assert!(config.is_blocked("app2", None)); + assert!(!config.is_blocked("app4", None)); + assert_eq!(config.blocked_count(), 3); + + // Test reasons + assert_eq!( + config.get_blocked_reason("app1", None), + Some(&"Test app 1".to_string()) + ); + assert_eq!(config.get_blocked_reason("app2", None), None); + assert_eq!( + config.get_blocked_reason("app3", None), + Some(&"Test app 3".to_string()) + ); +} + +#[test] +fn test_app_blocklist_version_match() { + let config_content = r#" + [[blocked_apps]] + name = "openssl@1.1.1" + reason = "Vulnerable version" + + [[blocked_apps]] + name = "nginx@1.20.0" + + [[blocked_apps]] + name = "libfoo@2.*" + reason = "Unsupported major version" + + strict = true + log_skipped = true + "#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + assert!(config.is_blocked("openssl", Some("1.1.1"))); + assert!(!config.is_blocked("openssl", Some("3.0.0"))); + assert!(config.is_blocked("nginx", Some("1.20.0"))); + assert!(!config.is_blocked("nginx", Some("1.21.0"))); + assert!(config.is_blocked("libfoo", Some("2.5.0"))); + assert!(!config.is_blocked("libfoo", Some("3.0.0"))); + assert_eq!(config.blocked_count(), 3); +} + +#[test] +fn test_app_blocklist_pattern_match() { + let config_content = r#" + [[blocked_apps]] + name = "test-*" + reason = "Test applications" + + [[blocked_apps]] + name = "deprecated-*" + reason = "Deprecated applications" + + [[blocked_apps]] + name = "nginx-*" + + strict = true + log_skipped = true + "#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + assert!(config.is_blocked("test-app", None)); + assert!(config.is_blocked("test-utils", None)); + assert!(config.is_blocked("deprecated-old", None)); + assert!(config.is_blocked("nginx-main", None)); + assert!(!config.is_blocked("new-app", None)); + assert_eq!(config.blocked_count(), 3); +} + +#[test] +fn test_app_blocklist_name_and_version_patterns() { + let config_content = r#" + [[blocked_apps]] + name = "test-*@1.*" + reason = "Test apps v1" + + [[blocked_apps]] + name = "deprecated-*" + + [[blocked_apps]] + name = "nginx@*" + reason = "All nginx versions" + + [[blocked_apps]] + name = "lib*@2.*" + + strict = true + log_skipped = true + "#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + // Versioned pattern matching + assert!(config.is_blocked("test-app", Some("1.5.0"))); + assert!(!config.is_blocked("test-app", Some("2.0.0"))); + assert!(config.is_blocked("deprecated-tool", None)); + assert!(config.is_blocked("nginx", Some("1.20.0"))); + assert!(config.is_blocked("nginx", Some("2.0.0"))); + assert!(config.is_blocked("libfoo", Some("2.1.0"))); + assert!(!config.is_blocked("libfoo", Some("3.0.0"))); + assert_eq!(config.blocked_count(), 4); +} + +#[test] +fn test_app_blocklist_empty() { + let config_content = r#" + strict = true + log_skipped = true + "#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + assert!(!config.is_blocked("any-app", None)); + assert!(!config.is_blocked("any-app", Some("1.0.0"))); + assert_eq!(config.blocked_count(), 0); +} + +#[test] +fn test_app_blocklist_file_not_found() { + let path = PathBuf::from("/nonexistent/path/app-blocklist.toml"); + let config = AppBlocklistConfigFile::load(&path).unwrap(); + + assert!(!config.is_blocked("any-app", None)); + assert_eq!(config.blocked_count(), 0); + assert!(config.strict); // Default value + assert!(config.log_skipped); // Default value +} + +#[test] +fn test_app_blocklist_invalid_toml() { + let config_content = r#" + This is not valid TOML + blocked_apps = ["app1"] + "#; + + let result = AppBlocklistConfigFile::load_from_str(config_content); + assert!(result.is_err()); +} + +#[test] +fn test_app_blocklist_non_strict_mode() { + let config_content = r#"strict = false +log_skipped = true + +[[blocked_apps]] +name = "app1" +reason = "Should be skipped" +"#; + + let config = AppBlocklistConfigFile::load_from_str(config_content).unwrap(); + + assert!(config.is_blocked("app1", None)); // Still detected as blocked + assert!(!config.strict); // Strict mode is off + assert!(config.log_skipped); + assert_eq!(config.blocked_count(), 1); +} diff --git a/dadk-config/tests/test_template_config.rs b/dadk-config/tests/test_template_config.rs new file mode 100644 index 0000000..159ab9b --- /dev/null +++ b/dadk-config/tests/test_template_config.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +#[cfg(test)] +mod tests { + use super::*; + use dadk_config::app_blocklist::AppBlocklistConfigFile; + use std::fs; + + #[test] + fn test_template_config_file() { + let template_path = PathBuf::from("templates/config/app-blocklist.toml"); + + // 检查文件是否存在 + assert!(template_path.exists(), "Template file should exist"); + + // 读取文件内容 + let content = + fs::read_to_string(&template_path).expect("Should be able to read template file"); + + // 尝试解析配置 + let config = + AppBlocklistConfigFile::load_from_str(&content).expect("Template should be valid TOML"); + + // 验证默认配置 + assert!(config.strict, "Default strict mode should be true"); + assert!(config.log_skipped, "Default log_skipped should be true"); + + // 验证有被屏蔽的应用程序 + assert!( + config.blocked_count() > 0, + "Template should contain example blocked apps" + ); + + println!("✅ Template configuration is valid!"); + println!("📊 Blocked apps count: {}", config.blocked_count()); + + // 测试一些匹配案例 + let test_cases = [ + ("busybox", None, true), + ("test-app", None, true), + ("test-example", None, true), // 应该匹配 "test-*" 模式 + ("openssl", Some("1.1.1"), true), + ("openssl", Some("3.0.0"), false), + ("nginx", Some("1.20.0"), true), // 应该匹配 "nginx@*" 模式 + ("old-app", None, true), // 应该匹配 "old-*" 模式 + ("app-debug", None, true), // 应该匹配 "*-debug" 模式 + ("libfoo", Some("2.5.0"), true), // 应该匹配 "lib*@2.*" 模式 + ("libfoo", Some("3.0.0"), false), + ("random-app", None, false), + ]; + + for (name, version, expected_blocked) in test_cases { + let blocked = config.is_blocked(name, version); + let version_str = version.map(|v| format!("@{}", v)).unwrap_or_default(); + let status = if blocked { "BLOCKED" } else { "ALLOWED" }; + println!(" - {}{}: {}", name, version_str, status); + + if expected_blocked { + assert!(blocked, "Expected {} to be blocked", name); + } else { + assert!(!blocked, "Expected {} to be allowed", name); + } + } + } +} diff --git a/dadk-user/Cargo.toml b/dadk-user/Cargo.toml index 22da699..93f130e 100644 --- a/dadk-user/Cargo.toml +++ b/dadk-user/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dadk-user" -version = "0.4.0" +version = "0.5.0" edition = "2021" description = "DragonOS Application Development Kit - user prog build" license = "GPL-2.0-only" @@ -9,7 +9,7 @@ license = "GPL-2.0-only" anyhow = { version = "1.0.90", features = ["std", "backtrace"] } chrono = { version = "=0.4.35", features = ["serde"] } clap = { version = "=4.5.20", features = ["derive"] } -dadk-config = { version = "0.4.0", path = "../dadk-config" } +dadk-config = { version = "0.5.0", path = "../dadk-config" } derive_builder = "0.20.0" lazy_static = "1.4.0" log = "0.4.17" diff --git a/dadk-user/src/context.rs b/dadk-user/src/context.rs index 508a759..2a14955 100644 --- a/dadk-user/src/context.rs +++ b/dadk-user/src/context.rs @@ -4,7 +4,9 @@ use std::{ sync::{Arc, Mutex, Weak}, }; -use dadk_config::{common::target_arch::TargetArch, user::UserCleanLevel}; +use dadk_config::{ + app_blocklist::AppBlocklistConfigFile, common::target_arch::TargetArch, user::UserCleanLevel, +}; use derive_builder::Builder; use log::error; #[cfg(test)] @@ -30,6 +32,9 @@ pub struct DadkUserExecuteContext { #[builder(default = "crate::DADKTask::default_target_arch()")] target_arch: TargetArch, + /// 应用程序黑名单配置 + app_blocklist: Option, + #[cfg(test)] base_test_context: Option, @@ -99,6 +104,14 @@ impl DadkUserExecuteContext { pub fn cache_dir(&self) -> Option<&PathBuf> { self.cache_dir.as_ref() } + + pub fn app_blocklist(&self) -> &Option { + &self.app_blocklist + } + + pub fn set_app_blocklist(&mut self, blocklist: AppBlocklistConfigFile) { + self.app_blocklist = Some(blocklist); + } } #[cfg(test)] @@ -145,6 +158,7 @@ impl TestContext for DadkExecuteContextTestBuildX86_64V1 { DadkUserExecuteContextBuilder::default_test_execute_context_builder(&base_context) .target_arch(TargetArch::X86_64) .config_dir(Some(base_context.config_v1_dir())) + .app_blocklist(None) .build() .expect("Failed to build DadkExecuteContextTestBuildX86_64V1"); let context = Arc::new(context); @@ -166,6 +180,7 @@ impl TestContext for DadkExecuteContextTestBuildRiscV64V1 { DadkUserExecuteContextBuilder::default_test_execute_context_builder(&base_context) .target_arch(TargetArch::RiscV64) .config_dir(Some(base_context.config_v1_dir())) + .app_blocklist(None) .build() .expect("Failed to build DadkExecuteContextTestBuildRiscV64V1"); let context = Arc::new(context); diff --git a/dadk-user/src/executor/tests.rs b/dadk-user/src/executor/tests.rs index 7423650..60d2a75 100644 --- a/dadk-user/src/executor/tests.rs +++ b/dadk-user/src/executor/tests.rs @@ -13,7 +13,8 @@ use crate::{ use super::create_global_env_list; fn setup_executor(config_file: PathBuf, ctx: &T) -> Executor { - let task = Parser::new(ctx.base_context().config_v2_dir()).parse_config_file(&config_file); + let task = + Parser::new(ctx.base_context().config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let scheduler = Scheduler::new( ctx.execute_context().self_ref().unwrap(), diff --git a/dadk-user/src/lib.rs b/dadk-user/src/lib.rs index 849069a..0bab8ab 100644 --- a/dadk-user/src/lib.rs +++ b/dadk-user/src/lib.rs @@ -83,7 +83,6 @@ //! - 支持自动更新 //! - 完善clean命令的逻辑 -#![feature(extract_if)] #![feature(io_error_more)] #[macro_use] @@ -133,7 +132,10 @@ pub fn dadk_user_main(context: DadkUserExecuteContext) { context.thread_num().map_or_else(|| 0, |t| t) ); - let mut parser = parser::Parser::new(context.config_dir().unwrap().clone()); + let mut parser = parser::Parser::new( + context.config_dir().unwrap().clone(), + context.app_blocklist().clone(), + ); let r = parser.parse(); if r.is_err() { exit(1); diff --git a/dadk-user/src/parser/mod.rs b/dadk-user/src/parser/mod.rs index 0f7ecc2..4020fe7 100644 --- a/dadk-user/src/parser/mod.rs +++ b/dadk-user/src/parser/mod.rs @@ -51,8 +51,8 @@ use std::{ use self::task::DADKTask; use anyhow::Result; -use dadk_config::user::UserConfigFile; -use log::{debug, error, info}; +use dadk_config::{app_blocklist::AppBlocklistConfigFile, user::UserConfigFile}; +use log::{debug, error, info, warn}; pub mod task; pub mod task_log; @@ -66,6 +66,8 @@ pub struct Parser { config_dir: PathBuf, /// 扫描到的配置文件列表 config_files: Vec, + /// 黑名单配置 + blocklist: Option, } pub struct ParserError { @@ -123,10 +125,11 @@ pub enum InnerParserError { } impl Parser { - pub fn new(config_dir: PathBuf) -> Self { + pub fn new(config_dir: PathBuf, blocklist: Option) -> Self { Self { config_dir, config_files: Vec::new(), + blocklist, } } @@ -146,8 +149,14 @@ impl Parser { let r: Result> = self.gen_tasks(); if r.is_err() { error!("Error while parsing config files: {:?}", r); + return r; } - return r; + let mut tasks = r.unwrap(); + + // 应用黑名单过滤 + self.filter_blocked_apps(&mut tasks)?; + + return Ok(tasks); } /// # 扫描配置文件目录,找到所有配置文件 @@ -235,4 +244,76 @@ impl Parser { let dadk_user_config = UserConfigFile::load(config_file)?; DADKTask::try_from(dadk_user_config) } + + /// 过滤黑名单中的应用程序 + fn filter_blocked_apps(&self, tasks: &mut Vec<(PathBuf, DADKTask)>) -> Result<()> { + let blocklist = match &self.blocklist { + Some(blocklist) if blocklist.blocked_count() > 0 => blocklist, + _ => return Ok(()), + }; + + if blocklist.log_skipped { + info!( + "Found {} applications in blocklist", + blocklist.blocked_count() + ); + } + + let mut skipped_apps = Vec::new(); + let mut remaining_tasks = Vec::new(); + + // 过滤任务 + for (config_path, task) in tasks.drain(..) { + if blocklist.is_blocked(&task.name, Some(&task.version)) { + skipped_apps.push(task.name.clone()); + + if blocklist.log_skipped { + if blocklist.strict { + warn!( + "Skipping blocked application '{}' (config: {})", + task.name, + config_path.display() + ); + } else { + warn!( + "Application '{}' is in blocklist but not skipped (strict mode off)", + task.name + ); + } + } + + // 只有在严格模式下才真正跳过 + if blocklist.strict { + continue; + } + } + remaining_tasks.push((config_path, task)); + } + + *tasks = remaining_tasks; + + // 输出摘要信息 + if !skipped_apps.is_empty() && blocklist.log_skipped { + if blocklist.strict { + info!( + "Skipped {} blocked applications: {}", + skipped_apps.len(), + skipped_apps.join(", ") + ); + } else { + info!( + "Found {} applications in blocklist but not skipped (strict mode off): {}", + skipped_apps.len(), + skipped_apps.join(", ") + ); + } + + // 输出所有blocked的应用 + for (idx, app) in skipped_apps.iter().enumerate() { + log::debug!("Blocked application {}: {}", idx + 1, app); + } + } + + Ok(()) + } } diff --git a/dadk-user/src/scheduler/tests.rs b/dadk-user/src/scheduler/tests.rs index 89e0295..ec9ae96 100644 --- a/dadk-user/src/scheduler/tests.rs +++ b/dadk-user/src/scheduler/tests.rs @@ -21,7 +21,8 @@ fn should_not_run_task_only_riscv64_on_x86_64(ctx: &DadkExecuteContextTestBuildX .base_context() .config_v2_dir() .join("app_target_arch_riscv64_only_0_2_0.toml"); - let task = Parser::new(ctx.base_context().config_v2_dir()).parse_config_file(&config_file); + let task = + Parser::new(ctx.base_context().config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let task = task.unwrap(); assert!( @@ -62,7 +63,8 @@ fn should_not_run_task_only_x86_64_on_riscv64(ctx: &DadkExecuteContextTestBuildR .base_context() .config_v2_dir() .join("app_target_arch_x86_64_only_0_2_0.toml"); - let task = Parser::new(ctx.base_context().config_v2_dir()).parse_config_file(&config_file); + let task = + Parser::new(ctx.base_context().config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let task = task.unwrap(); assert!( @@ -103,7 +105,8 @@ fn should_run_task_include_x86_64_on_x86_64(ctx: &DadkExecuteContextTestBuildX86 .base_context() .config_v2_dir() .join("app_all_target_arch_0_2_0.toml"); - let task = Parser::new(ctx.base_context().config_v2_dir()).parse_config_file(&config_file); + let task = + Parser::new(ctx.base_context().config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let task = task.unwrap(); @@ -136,7 +139,8 @@ fn should_run_task_include_riscv64_on_riscv64(ctx: &DadkExecuteContextTestBuildR .base_context() .config_v2_dir() .join("app_all_target_arch_0_2_0.toml"); - let task = Parser::new(ctx.base_context().config_v2_dir()).parse_config_file(&config_file); + let task = + Parser::new(ctx.base_context().config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let task = task.unwrap(); @@ -166,7 +170,7 @@ fn should_run_task_include_riscv64_on_riscv64(ctx: &DadkExecuteContextTestBuildR #[test] fn ensure_all_target_arch_testcase_v1(ctx: &BaseGlobalTestContext) { let config_file = ctx.config_v2_dir().join("app_all_target_arch_0_2_0.toml"); - let task = Parser::new(ctx.config_v2_dir()).parse_config_file(&config_file); + let task = Parser::new(ctx.config_v2_dir(), None).parse_config_file(&config_file); assert!(task.is_ok(), "parse error: {:?}", task); let task = task.unwrap(); diff --git a/dadk/Cargo.toml b/dadk/Cargo.toml index 027745e..3af11f3 100644 --- a/dadk/Cargo.toml +++ b/dadk/Cargo.toml @@ -6,7 +6,7 @@ authors = [ "xuzihao " ] -version = "0.4.0" +version = "0.5.0" edition = "2021" description = "DragonOS Application Development Kit\nDragonOS应用开发工具" license = "GPL-2.0-only" @@ -33,8 +33,8 @@ insiders = [] anyhow = { version = "1.0.90", features = ["std", "backtrace"] } clap = { version = "4.5.20", features = ["derive"] } crossbeam = "0.8.4" -dadk-config = { version = "0.4.0", path = "../dadk-config" } -dadk-user = { version = "0.4.0", path = "../dadk-user" } +dadk-config = { version = "0.5.0", path = "../dadk-config" } +dadk-user = { version = "0.5.0", path = "../dadk-user" } derive_builder = "0.20.0" env_logger = { workspace = true } humantime = "2.1.0" diff --git a/dadk/src/actions/user.rs b/dadk/src/actions/user.rs index 6b085cc..7cb86cc 100644 --- a/dadk/src/actions/user.rs +++ b/dadk/src/actions/user.rs @@ -9,6 +9,9 @@ pub(super) fn run(ctx: &DADKExecContext, cmd: &UserCommand) -> Result<()> { let sysroot_dir = ctx.sysroot_dir()?; let dadk_user_action: dadk_user::context::Action = cmd.clone().into(); + // 获取应用程序黑名单配置 + let app_blocklist = ctx.app_blocklist().clone(); + let context = dadk_user::context::DadkUserExecuteContextBuilder::default() .sysroot_dir(sysroot_dir) .config_dir(config_dir) @@ -16,6 +19,7 @@ pub(super) fn run(ctx: &DADKExecContext, cmd: &UserCommand) -> Result<()> { .thread_num(1) .cache_dir(cache_root_dir) .target_arch(ctx.target_arch()) + .app_blocklist(app_blocklist) .build() .expect("Failed to build execute context"); dadk_user_main(context); diff --git a/dadk/src/context/mod.rs b/dadk/src/context/mod.rs index 54fc3ef..03ad045 100644 --- a/dadk/src/context/mod.rs +++ b/dadk/src/context/mod.rs @@ -3,9 +3,11 @@ use std::{cell::OnceCell, path::PathBuf}; use anyhow::Result; use clap::Parser; use dadk_config::{ - common::target_arch::TargetArch, manifest::DadkManifestFile, rootfs::RootFSConfigFile, + app_blocklist::AppBlocklistConfigFile, common::target_arch::TargetArch, + manifest::DadkManifestFile, rootfs::RootFSConfigFile, }; use derive_builder::Builder; +use log::warn; use manifest::parse_manifest; use crate::{ @@ -24,6 +26,10 @@ pub struct DADKExecContext { /// RootFS config file rootfs: OnceCell, + + /// Application blocklist config file + #[builder(setter(skip))] + app_blocklist: OnceCell, } pub fn build_exec_context() -> Result { @@ -92,6 +98,36 @@ impl DADKExecContext { self.manifest().metadata.arch } + /// 获取应用程序黑名单配置 + pub fn app_blocklist(&self) -> &AppBlocklistConfigFile { + self.app_blocklist.get_or_init(|| { + let manifest = self.manifest(); + let config_path = &manifest.metadata.app_blocklist_config; + + // 转换为绝对路径 + let abs_config_path = if config_path.is_absolute() { + config_path.clone() + } else { + self.workdir().join(config_path) + }; + + AppBlocklistConfigFile::load(&abs_config_path).unwrap_or_else(|e| { + warn!( + "Failed to load app blocklist config: {}, using empty config", + e + ); + AppBlocklistConfigFile::load_from_str( + r#" + blocked_apps = [] + strict = true + log_skipped = true + "#, + ) + .unwrap_or_default() + }) + }) + } + /// 获取磁盘镜像的路径,路径由工作目录、架构和固定文件名组成 pub fn disk_image_path(&self) -> PathBuf { self.workdir() diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a87844e..0b94613 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -51,6 +51,7 @@ export default defineUserConfig({ '/user-manual/quickstart.md', '/user-manual/profiling.md', '/user-manual/user-prog-build.md', + '/user-manual/app-blocklist.md', '/user-manual/envs.md', ] } diff --git a/docs/user-manual/README.md b/docs/user-manual/README.md index fa252c5..db3ef1f 100644 --- a/docs/user-manual/README.md +++ b/docs/user-manual/README.md @@ -9,3 +9,4 @@ - [对DragonOS内核进行性能分析](./profiling.md) - [构建用户程序](./user-prog-build.md) - [环境变量](./envs.md) +- [应用程序黑名单功能](./app-blocklist.md) diff --git a/docs/user-manual/app-blocklist.md b/docs/user-manual/app-blocklist.md new file mode 100644 index 0000000..fbaccbc --- /dev/null +++ b/docs/user-manual/app-blocklist.md @@ -0,0 +1,187 @@ +# 应用程序黑名单 + +## 功能概述 + +DADK 支持应用程序黑名单功能,允许用户指定不希望编译和安装的应用程序。当黑名单中的应用程序被检测到时,DADK 会自动跳过这些应用程序的构建和安装过程。 + +## 配置文件 + +### 1. 创建黑名单配置文件 + +在项目根目录的 `config/` 目录下创建 `app-blocklist.toml` 文件: + +```toml +# 应用程序黑名单配置文件 +# 路径: config/app-blocklist.toml + +# 是否启用严格模式(可选) +# strict = false # 非严格模式:只警告不跳过 +# strict = true # 严格模式:跳过并警告(默认) +strict = true + +# 是否在日志中显示被跳过的应用(可选) +log_skipped = true + +# 被屏蔽的应用程序列表 + +[[blocked_apps]] +name = "openssl@1.1.1" +reason = "存在安全漏洞,请使用3.x版本" + + +[[blocked_apps]] +name = "deprecated-old-app" +``` + +### 2. 配置文件路径(可选) + +如果需要自定义黑名单配置文件的路径,可以在 `dadk-manifest.toml` 中指定: + +```toml +[metadata] +arch = "x86_64" +rootfs-config = "config/rootfs.toml" +boot-config = "config/boot.toml" +hypervisor-config = "config/hypervisor.toml" +sysroot-dir = "bin/sysroot" +cache-root-dir = "bin/dadk_cache" +app-blocklist-config = "config/my-blocklist.toml" # 自定义路径 +``` + +## 使用示例 + +### 基本用法 + +创建黑名单配置文件 `config/app-blocklist.toml`: + +```toml +strict = true +log_skipped = true + +[[blocked_apps]] +name = "busybox" +reason = "Skipping busybox and test applications" + +[[blocked_apps]] +name = "test-app" +``` + +### 输出示例 + +当黑名单中有应用程序时,DADK 会输出类似以下日志: + +``` +[INFO] Found 2 applications in blocklist +[WARN] Skipping blocked application 'busybox' (config: config/busybox.toml) +[WARN] Skipping blocked application 'test-app' (config: config/test-app.toml) +[INFO] Skipped 2 blocked applications: busybox, test-app +[DEBUG] Blocklist reasons: +busybox: Skipping busybox and test applications +``` + +## 高级功能 + +### 1. 模式匹配 + +黑名单支持通配符模式匹配: + +```toml +strict = true +log_skipped = true + +[[blocked_apps]] +name = "test-*" +reason = "所有测试应用" + +[[blocked_apps]] +name = "deprecated-*" +reason = "已弃用的应用" + +[[blocked_apps]] +name = "nginx-*" +reason = "所有nginx相关应用" +``` + +### 2. 版本匹配 + +支持指定特定版本的应用。匹配优先级为:精确匹配 > 版本匹配 > 模式匹配: + +```toml +strict = true +log_skipped = true + +[[blocked_apps]] +name = "openssl@1.1.1" +reason = "存在安全漏洞的版本" + +[[blocked_apps]] +name = "libfoo@2.*" +reason = "不支持的2.x版本" +``` + +### 3. 非严格模式 + +如果只想记录警告但不跳过应用程序的构建和安装,可以设置 `strict = false`。注意:即使在非严格模式下,应用仍然会被检测为"被屏蔽",只是不会实际跳过构建: + +```toml +strict = false +log_skipped = true + +[[blocked_apps]] +name = "deprecated-app" +reason = "应该被替换的旧应用" +``` + +### 4. 静默模式 + +如果不想显示被跳过的应用程序,可以设置 `log_skipped = false`: + +```toml +strict = true +log_skipped = false + +[[blocked_apps]] +name = "internal-tools" +reason = "内部工具,不显示在日志中" +``` + +## 注意事项 + +1. **依赖关系**:如果其他应用程序依赖被屏蔽的应用程序,构建过程可能会失败。在使用黑名单功能前,请仔细检查应用程序间的依赖关系,确保没有其他应用依赖被屏蔽的应用。 + +2. **配置文件格式**:确保 `app-blocklist.toml` 是有效的 TOML 格式。必须使用数组表格格式 `[[blocked_apps]]`,每个应用程序一个条目,这样可以为每个应用独立设置reason。 + +3. **文件路径**:黑名单配置文件路径可以是相对路径或绝对路径。相对路径相对于工作目录。完整的模板配置文件请参考:[app-blocklist.toml](https://github.com/DragonOS-Community/DADK/blob/main/dadk-config/templates/config/app-blocklist.toml) + +4. **模板文件**:DADK提供了完整的模板配置文件,位于 `dadk-config/templates/config/app-blocklist.toml`,其中包含了详细的注释和各种使用场景的示例。 + +4. **模板文件**:DADK提供了完整的模板配置文件,位于 `dadk-config/templates/config/app-blocklist.toml`,其中包含了详细的注释和各种使用场景的示例。 + +5. **字段顺序**:建议将 `strict` 和 `log_skipped` 等全局配置放在文件开头,`[[blocked_apps]]` 数组放在后面。 + +## 故障排除 + +### 问题:黑名单配置文件加载失败 + +如果看到以下警告: +``` +[WARN] Failed to load app blocklist config: ..., using empty config +``` + +请检查: +- 配置文件路径是否正确 +- 文件是否存在 +- TOML 格式是否正确 + +### 问题:应用程序未被跳过 + +请检查: +- 应用程序名称是否准确匹配(区分大小写) +- `strict` 是否设置为 `true` +- 配置文件是否被正确加载 + +### 问题:构建失败,提示依赖缺失 + +如果其他应用程序依赖被屏蔽的应用程序,需要: +- 从黑名单中移除该应用 +- 或修改依赖应用程序的配置,移除对被屏蔽应用的依赖 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7a2c6fa..1a39d71 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2024-11-05" -components = ["rust-src", "rustfmt"] \ No newline at end of file +channel = "nightly-2025-08-10" +components = ["rust-src", "rustfmt"]