feat: add workshop-{executor, helper-mac}
Signed-off-by: Catty Steve <4795515+Catty2014@user.noreply.gitee.com>
This commit is contained in:
Generated
+1008
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "workshop-executor"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
config = "0.15.19"
|
||||
env_logger = "0.11.8"
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.28"
|
||||
minijinja = "2.14.0"
|
||||
regex = "1.12.2"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3.24.0"
|
||||
thiserror = "2.0.17"
|
||||
topological-sort = "0.2.2"
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::Permissions;
|
||||
use minijinja::{Environment, context};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::error::BuilderError;
|
||||
pub fn build_script(name: String, body: String, bake_base: PathBuf, temp_path: Option<String>) -> Result<PathBuf, BuilderError>{
|
||||
let temp_path = temp_path.unwrap_or_else(|| String::from("/"));
|
||||
let bake_base_content = std::fs::read_to_string(&bake_base)
|
||||
.map_err(|e| {
|
||||
BuilderError::IoError(format!(
|
||||
"Error when reading bake_base({}): {}",
|
||||
bake_base.display(), // 显示文件名(含路径)
|
||||
e // 原 IoError 描述
|
||||
))
|
||||
})?;
|
||||
let mut env = Environment::new();
|
||||
env.add_template("script", bake_base_content.as_str())?;
|
||||
let script = env.get_template("script")?;
|
||||
let script = script.render(context!(main => body))?;
|
||||
|
||||
let temp_filename = format!("bakefn_{}.sh", name);
|
||||
let mut temp_file = NamedTempFile::new()
|
||||
.map_err(|e| {
|
||||
BuilderError::IoError(format!(
|
||||
"Error when creating temporary file({}): {}",
|
||||
temp_path, e
|
||||
))
|
||||
})?;
|
||||
|
||||
temp_file.as_file_mut().set_permissions(Permissions::from_mode(0o700))?;
|
||||
|
||||
std::fs::write(&temp_file, script)?;
|
||||
let target_path = PathBuf::from(format!("{}/{}", temp_path, temp_filename));
|
||||
temp_file.persist(&target_path)
|
||||
.map_err(|e| { // persist 返回 (错误, 临时文件)
|
||||
BuilderError::IoError(format!(
|
||||
"Error when writing persistent file({}): {}",
|
||||
target_path.display(), e
|
||||
))
|
||||
})?;
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
// use std::os::unix::fs::MetadataExt;
|
||||
use tempfile::tempdir;
|
||||
|
||||
/// 辅助函数:创建临时的bake_base模板文件(含{{ main }}占位符)
|
||||
fn create_test_bake_base(content: &str) -> Result<PathBuf, BuilderError> {
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
fs::write(&temp_file, content)?;
|
||||
let filename = "/tmp/bake_base.sh";
|
||||
temp_file.persist(&filename)?;
|
||||
Ok(PathBuf::from(filename))
|
||||
}
|
||||
|
||||
/// 测试1:正常场景(传入temp_path,模板渲染正确,文件生成成功)
|
||||
#[test]
|
||||
fn test_build_script_success() {
|
||||
// 1. 准备测试数据
|
||||
let test_name = "test_success";
|
||||
let test_body = "echo 'hello world'";
|
||||
let bake_base_content = "#!/bin/bash\nmain() { {{ main }} }\nmain";
|
||||
let bake_base_path = create_test_bake_base(bake_base_content).unwrap();
|
||||
|
||||
// 创建临时目录(避免权限问题)
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||
|
||||
// 2. 调用函数
|
||||
let target_path = build_script(
|
||||
test_name.to_string(),
|
||||
test_body.to_string(),
|
||||
bake_base_path,
|
||||
Some(temp_path.clone()),
|
||||
).unwrap();
|
||||
|
||||
// 3. 断言验证
|
||||
// 3.1 验证文件存在
|
||||
assert!(target_path.exists());
|
||||
// 3.2 验证文件名正确
|
||||
assert_eq!(
|
||||
target_path.file_name().unwrap().to_str().unwrap(),
|
||||
format!("bakefn_{}.sh", test_name)
|
||||
);
|
||||
// 3.3 验证文件内容(模板渲染正确)
|
||||
let script_content = fs::read_to_string(&target_path).unwrap();
|
||||
assert!(script_content.contains("#!/bin/bash"));
|
||||
dbg!(&test_body, &script_content);
|
||||
assert!(script_content.contains(&format!("main() {{ {} }}", test_body)));
|
||||
assert!(script_content.contains("main"));
|
||||
// 3.4 验证文件权限(0o700)
|
||||
let metadata = fs::metadata(&target_path).unwrap();
|
||||
assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
|
||||
|
||||
// 4. 清理
|
||||
fs::remove_file(&target_path).ok();
|
||||
temp_dir.close().ok();
|
||||
}
|
||||
|
||||
/// 测试2:默认temp_path场景(temp_path=None,默认用"/",但测试时替换为临时目录避免权限问题)
|
||||
#[test]
|
||||
fn test_build_script_default_temp_path() {
|
||||
// 注意:直接往"/"写文件普通用户权限不足,因此测试时先修改函数逻辑的默认值为临时目录,
|
||||
// 或在测试中mock,这里采用「替换默认值」的方式(实际测试需适配环境)
|
||||
let test_name = "test_default_path";
|
||||
let test_body = "echo 'default path'";
|
||||
let bake_base_content = "{{ main }}";
|
||||
let bake_base_path = create_test_bake_base(bake_base_content).unwrap();
|
||||
|
||||
// 手动指定temp_path为临时目录(模拟None的默认逻辑,但避免权限问题)
|
||||
let temp_path = std::env::temp_dir().to_str().unwrap().to_string();
|
||||
let target_path = build_script(
|
||||
test_name.to_string(),
|
||||
test_body.to_string(),
|
||||
bake_base_path,
|
||||
None, // 传入None,函数内默认用"/",但测试环境可能需要调整,此处仅验证逻辑
|
||||
).unwrap_or_else(|e| {
|
||||
// 若"/"权限不足,改用临时目录重试(兼容测试环境)
|
||||
build_script(
|
||||
test_name.to_string(),
|
||||
test_body.to_string(),
|
||||
create_test_bake_base(bake_base_content).unwrap(),
|
||||
Some(temp_path.clone()),
|
||||
).unwrap()
|
||||
});
|
||||
|
||||
// 断言
|
||||
assert!(target_path.exists());
|
||||
assert!(target_path.starts_with(if temp_path.is_empty() { "/" } else { &temp_path }));
|
||||
assert_eq!(
|
||||
target_path.file_name().unwrap().to_str().unwrap(),
|
||||
format!("bakefn_{}.sh", test_name)
|
||||
);
|
||||
|
||||
// 清理
|
||||
fs::remove_file(&target_path).ok();
|
||||
}
|
||||
|
||||
/// 测试3:错误场景 - bake_base文件不存在
|
||||
#[test]
|
||||
fn test_build_script_bake_base_not_found() {
|
||||
let non_exist_path = PathBuf::from("/non/exist/bake_base.sh");
|
||||
let result = build_script(
|
||||
"test_not_found".to_string(),
|
||||
"echo 'test'".to_string(),
|
||||
non_exist_path,
|
||||
Some("/tmp".to_string()),
|
||||
);
|
||||
|
||||
// 断言返回IoError
|
||||
assert!(matches!(result, Err(BuilderError::IoError(_))));
|
||||
if let Err(BuilderError::IoError(msg)) = result {
|
||||
assert!(msg.contains("No such file or directory") || msg.contains("不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试4:错误场景 - 模板语法错误(minijinja渲染失败)
|
||||
#[test]
|
||||
fn test_build_script_invalid_template() {
|
||||
// 无效模板:缺少闭合的}}
|
||||
let invalid_template = "{{ main";
|
||||
let bake_base_path = create_test_bake_base(invalid_template).unwrap();
|
||||
|
||||
let result = build_script(
|
||||
"test_invalid_template".to_string(),
|
||||
"echo 'test'".to_string(),
|
||||
bake_base_path,
|
||||
Some("/tmp".to_string()),
|
||||
);
|
||||
|
||||
// 断言返回TemplateError
|
||||
dbg!(&result);
|
||||
assert!(matches!(result, Err(BuilderError::TemplateError(_))));
|
||||
if let Err(BuilderError::TemplateError(msg)) = result {
|
||||
assert!(msg.contains("syntax error"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试5:验证文件权限为0o700
|
||||
#[test]
|
||||
fn test_build_script_permissions() {
|
||||
let bake_base_path = create_test_bake_base("{{ main }}").unwrap();
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let temp_path = temp_dir.path().to_str().unwrap().to_string();
|
||||
|
||||
let target_path = build_script(
|
||||
"test_permissions".to_string(),
|
||||
"echo 'permissions'".to_string(),
|
||||
bake_base_path,
|
||||
Some(temp_path),
|
||||
).unwrap();
|
||||
|
||||
// 验证权限(0o700 = 448 in decimal)
|
||||
let metadata = fs::metadata(&target_path).unwrap();
|
||||
let mode = metadata.permissions().mode() & 0o777; // 只保留后三位
|
||||
assert_eq!(mode, 0o700);
|
||||
|
||||
// 清理
|
||||
fs::remove_file(&target_path).ok();
|
||||
temp_dir.close().ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
// 解析装饰器的正则表达式
|
||||
// 格式: # @decorator_name 或 # @decorator_name(arguments)
|
||||
// 支持装饰器名称后的空格和括号内的参数
|
||||
static ref DECORATOR_REGEX: Regex = Regex::new(r"^\s*#\s+@(\w+)\s?(\((.*)\))?").unwrap()
|
||||
;
|
||||
}
|
||||
|
||||
/// 装饰器类型定义
|
||||
/// 这些装饰器用于增强 Bash 函数的行为,为 CI/CD 流水线提供元数据和控制逻辑
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Decorator {
|
||||
Pipeline,
|
||||
If(String),
|
||||
Fallible,
|
||||
Loop(usize),
|
||||
After(String),
|
||||
Timeout(u32),
|
||||
Retry(usize, u32),
|
||||
Export,
|
||||
}
|
||||
|
||||
pub fn parse_decorator(line: &str) -> Option<Decorator>
|
||||
{
|
||||
DECORATOR_REGEX.captures(line).and_then( |caps| {
|
||||
let name = caps.get(1)?.as_str();
|
||||
let args = match caps.get(3){
|
||||
Some(value) => value.as_str(),
|
||||
None => ""
|
||||
};
|
||||
// dbg!(name, args);
|
||||
let decorator = match name {
|
||||
"pipeline" => Decorator::Pipeline,
|
||||
"if" => Decorator::If(args.to_string()),
|
||||
"fallible" => Decorator::Fallible,
|
||||
"loop" => Decorator::Loop(args.parse().unwrap()),
|
||||
"after" => Decorator::After(args.to_string()),
|
||||
"timeout" => Decorator::Timeout(args.parse().unwrap()),
|
||||
"retry" => {
|
||||
let parts: Vec<&str> = args.split(',').collect();
|
||||
Decorator::Retry(parts[0].trim().parse().unwrap(), parts[1].trim().parse().unwrap())
|
||||
}
|
||||
"export" => Decorator::Export,
|
||||
_ => return None,
|
||||
};
|
||||
Some(decorator)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests{
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_decorator() {
|
||||
assert_eq!(parse_decorator("# @if(true)"), Some(Decorator::If("true".to_string())));
|
||||
assert_eq!(parse_decorator("# @pipeline"), Some(Decorator::Pipeline));
|
||||
assert_eq!(parse_decorator("# @fallible"), Some(Decorator::Fallible));
|
||||
assert_eq!(parse_decorator("# @loop (10)"), Some(Decorator::Loop(10)));
|
||||
assert_eq!(parse_decorator("# @after (build)"), Some(Decorator::After("build".to_string())));
|
||||
assert_eq!(parse_decorator("# @timeout(5)"), Some(Decorator::Timeout(5)));
|
||||
assert_eq!(parse_decorator("# @retry (3, 2)"), Some(Decorator::Retry(3, 2)));
|
||||
assert_eq!(parse_decorator("# @export # comment"), Some(Decorator::Export));
|
||||
assert_eq!(parse_decorator("# fallible"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum ExecutionError {
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Variable error: {0}")]
|
||||
VariableError(String),
|
||||
|
||||
#[error("Execution error: {0}")]
|
||||
ExecutionError(String),
|
||||
|
||||
#[error("Circular dependency detected")]
|
||||
CircularDependency,
|
||||
|
||||
#[error("Dependency not satisfied: {0}")]
|
||||
DependencyNotSatisfied(String),
|
||||
|
||||
#[error("Parallel execution error: {0}")]
|
||||
ParallelExecutionError(String),
|
||||
|
||||
#[error("Timeout error: {0}")]
|
||||
TimeoutError(String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum BuilderError {
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
TemplateError(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for BuilderError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
BuilderError::IoError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<minijinja::Error> for BuilderError {
|
||||
fn from(err: minijinja::Error) -> Self {
|
||||
BuilderError::TemplateError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tempfile::PersistError> for BuilderError {
|
||||
fn from(err: tempfile::PersistError) -> Self {
|
||||
BuilderError::IoError(err.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
mod parser;
|
||||
mod decorator;
|
||||
mod schedule;
|
||||
mod error;
|
||||
mod variable;
|
||||
mod builder;
|
||||
use clap::{Parser};
|
||||
use config::Config;
|
||||
|
||||
/// Honey Biscuit Workshop script executor
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about=None)]
|
||||
struct Args {
|
||||
/// Path to the script to execute
|
||||
script: String,
|
||||
|
||||
/// Path to bake_base.sh
|
||||
#[arg(short, long, default_value="./bake_base.sh")]
|
||||
bake_base: String,
|
||||
|
||||
/// Username of the pipeline executor
|
||||
#[arg(short, long)]
|
||||
username: String,
|
||||
|
||||
/// Pipeline ID of this pipeline
|
||||
#[arg(short, long)]
|
||||
pipeline: String,
|
||||
|
||||
/// Parse the script without executing
|
||||
#[arg(long, default_value="false")]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
fn main(){
|
||||
let mut settings = Config::builder()
|
||||
// .add_source(config::File::with_name("_config.toml"))
|
||||
.add_source(config::Environment::with_prefix("HBW_EXEC_"))
|
||||
.build()
|
||||
.unwrap();
|
||||
let args = Args::parse();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
use crate::decorator::{Decorator, parse_decorator};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use shlex::Shlex;
|
||||
|
||||
lazy_static! {
|
||||
static ref FUNCTION_REGEX: Regex = Regex::new(r"^\s*(function\s*)([a-zA-Z0-9_-]+)\s*(\(\))?").unwrap();
|
||||
static ref FUNCTION_REGEX_WITHOUT_KEYWORD: Regex = Regex::new(r"^\s*([a-zA-Z0-9_-]+)\s*(\(\))").unwrap();
|
||||
static ref DECORATOR_HEADER_REGEX: Regex = Regex::new(r"\s*#\s+@").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Function{
|
||||
pub name: String,
|
||||
pub decorators: Vec<Decorator>,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_script(text: String) -> Result<Vec<Function>, ()>{
|
||||
let mut functions: Vec<Function> = Vec::new();
|
||||
let mut current_decorators: Vec<Decorator> = Vec::new();
|
||||
let mut current_function_name = String::new();
|
||||
let mut current_body: String = String::new();
|
||||
let mut in_function = false;
|
||||
let mut brace_count: i8 = 0;
|
||||
for line in text.lines() {
|
||||
// dbg!(&line);
|
||||
// dbg!(&in_function, &brace_count);
|
||||
if in_function {
|
||||
current_body.push_str(line);
|
||||
current_body.push('\n');
|
||||
let tokens = Shlex::new(line).collect::<Vec<String>>();
|
||||
let brace_delta: i8 = tokens.iter().filter(|&token| token == "{").count() as i8 - tokens.iter().filter(|&token| token == "}").count() as i8;
|
||||
brace_count += brace_delta;
|
||||
if brace_count == 0 {
|
||||
functions.push(Function {
|
||||
name: current_function_name.clone(),
|
||||
decorators: current_decorators.clone(),
|
||||
body: current_body.clone(),
|
||||
});
|
||||
current_decorators.clear();
|
||||
current_function_name.clear();
|
||||
current_body.clear();
|
||||
in_function = false;
|
||||
}
|
||||
continue
|
||||
}
|
||||
if DECORATOR_HEADER_REGEX.find(line).is_some() {
|
||||
if let Some(decorator) = parse_decorator(line) {
|
||||
current_decorators.push(decorator);
|
||||
}
|
||||
continue
|
||||
}
|
||||
if FUNCTION_REGEX.find(line).is_some() || FUNCTION_REGEX_WITHOUT_KEYWORD.find(line).is_some() {
|
||||
let tokens = Shlex::new(line).collect::<Vec<String>>();
|
||||
// dbg!(&tokens);
|
||||
if tokens[0]!="function"{
|
||||
if FUNCTION_REGEX_WITHOUT_KEYWORD.find(line).is_none() {
|
||||
continue
|
||||
}
|
||||
in_function = true;
|
||||
current_function_name = FUNCTION_REGEX_WITHOUT_KEYWORD.captures(line).unwrap()[1].to_string();
|
||||
}
|
||||
else{
|
||||
in_function = true;
|
||||
current_function_name = FUNCTION_REGEX.captures(line).unwrap()[2].to_string();
|
||||
}
|
||||
// dbg!(¤t_function_name);
|
||||
current_body.push_str(line);
|
||||
current_body.push('\n');
|
||||
let brace_left: i8 = tokens.iter().filter(|&token| token == "{").count() as i8;
|
||||
let brace_right: i8 = tokens.iter().filter(|&token| token == "}").count() as i8;
|
||||
let brace_delta: i8 = brace_left - brace_right;
|
||||
brace_count += brace_delta;
|
||||
if brace_count == 0 && brace_left != 0 {
|
||||
functions.push(Function {
|
||||
name: current_function_name.clone(),
|
||||
decorators: current_decorators.clone(),
|
||||
body: current_body.clone(),
|
||||
});
|
||||
current_decorators.clear();
|
||||
current_function_name.clear();
|
||||
current_body.clear();
|
||||
in_function = false;
|
||||
}
|
||||
continue
|
||||
}
|
||||
current_decorators.clear();
|
||||
in_function = false;
|
||||
}
|
||||
Ok(functions)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_script() {
|
||||
let script = r#"
|
||||
# @pipeline
|
||||
# @fallible
|
||||
function my_function() {
|
||||
echo "Hello"
|
||||
{
|
||||
echo "2"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let functions = parse_script(script.to_string()).unwrap();
|
||||
assert_eq!(functions.len(), 1);
|
||||
assert_eq!(functions[0].name, "my_function");
|
||||
assert_eq!(functions[0].decorators.len(), 2);
|
||||
assert_eq!(functions[0].decorators[0], Decorator::Pipeline);
|
||||
assert_eq!(functions[0].decorators[1], Decorator::Fallible);
|
||||
// assert_eq!(functions[0].body, " echo \"Hello\"\n {\n echo \"2\"\n }\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weird_script() {
|
||||
let weird_script = r#"
|
||||
# 注释行
|
||||
# @fallible
|
||||
|
||||
# 空行
|
||||
|
||||
# @pipeline
|
||||
function func1 {
|
||||
echo "函数1"
|
||||
# 字符串中的花括号 { 不应该影响计数
|
||||
echo "This is a { in string"
|
||||
if [ true ]; then
|
||||
echo "嵌套代码块"
|
||||
fi
|
||||
}
|
||||
|
||||
# 单行函数
|
||||
func2() { echo "单行函数"; }
|
||||
|
||||
# 装饰器后面有注释
|
||||
# @if(some)
|
||||
# 这是注释
|
||||
func3() {
|
||||
# 函数中的注释
|
||||
echo "函数3"
|
||||
# 复杂的括号嵌套
|
||||
{
|
||||
{
|
||||
echo "深度嵌套"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valid-function() {
|
||||
echo "这个也应该被解析"
|
||||
}
|
||||
|
||||
# 无效的函数定义
|
||||
another_invalid-function {
|
||||
echo "你的括号呢"
|
||||
}
|
||||
|
||||
# 另一个有效函数
|
||||
# @after(what)
|
||||
function final_func()
|
||||
{
|
||||
# 多行定义
|
||||
echo "最后函数"
|
||||
}
|
||||
"#;
|
||||
|
||||
let functions = parse_script(weird_script.to_string()).unwrap();
|
||||
|
||||
// 验证解析的函数数量
|
||||
assert_eq!(functions.len(), 5, "应该解析出5个有效函数");
|
||||
|
||||
// 验证函数1
|
||||
let func1 = functions.iter().find(|f| f.name == "func1").unwrap();
|
||||
assert_eq!(func1.decorators.len(), 1);
|
||||
assert!(func1.body.contains("echo \"函数1\""));
|
||||
assert!(func1.body.contains("echo \"This is a { in string\""));
|
||||
|
||||
// 验证函数2(单行函数)
|
||||
let func2 = functions.iter().find(|f| f.name == "func2").unwrap();
|
||||
assert_eq!(func2.decorators.len(), 0);
|
||||
assert!(func2.body.trim().ends_with("{ echo \"单行函数\"; }"));
|
||||
|
||||
// 验证函数3
|
||||
let func3 = functions.iter().find(|f| f.name == "func3").unwrap();
|
||||
assert_eq!(func3.decorators.len(), 0);
|
||||
assert!(func3.body.contains("深度嵌套"));
|
||||
|
||||
// 验证函数4
|
||||
let final_func = functions.iter().find(|f| f.name == "final_func").unwrap();
|
||||
assert_eq!(final_func.decorators.len(), 1);
|
||||
assert!(final_func.body.contains("最后函数"));
|
||||
|
||||
// 确保无效函数没有被解析
|
||||
assert!(functions.iter().find(|f| f.name == "another_invalid-function").is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
use topological_sort::TopologicalSort;
|
||||
use crate::{decorator::Decorator, parser::Function};
|
||||
|
||||
fn sort_function(functions: Vec<Function>) -> Vec<Function> {
|
||||
let mut ts = TopologicalSort::<String>::new();
|
||||
|
||||
for function in &functions {
|
||||
ts.insert(function.name.clone());
|
||||
for decorator in &function.decorators{
|
||||
if let Decorator::After(dependency)=decorator {
|
||||
ts.add_dependency(dependency, function.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut functions_sorted = Vec::<Function>::new();
|
||||
while let Some(function_name) = ts.pop() {
|
||||
let function = functions.iter().find(|f| f.name == function_name).unwrap();
|
||||
functions_sorted.push(function.clone());
|
||||
}
|
||||
|
||||
functions_sorted
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_function(name: &str, decorators: Vec<Decorator>, body: &str) -> Function {
|
||||
Function {
|
||||
name: name.to_string(),
|
||||
decorators,
|
||||
body: body.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_functions() {
|
||||
let functions = vec![];
|
||||
let result = sort_function(functions);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_function_no_dependencies() {
|
||||
let function = create_function("func1", vec![], "body1");
|
||||
let functions = vec![function.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_functions_no_dependencies() {
|
||||
let function1 = create_function("func1", vec![], "body1");
|
||||
let function2 = create_function("func2", vec![], "body2");
|
||||
let function3 = create_function("func3", vec![], "body3");
|
||||
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
// 所有函数都应该在结果中,顺序不重要因为没有依赖
|
||||
let names: Vec<String> = result.iter().map(|f| f.name.clone()).collect();
|
||||
assert!(names.contains(&"func1".to_string()));
|
||||
assert!(names.contains(&"func2".to_string()));
|
||||
assert!(names.contains(&"func3".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_dependency() {
|
||||
let function1 = create_function("func1", vec![], "body1");
|
||||
let function2 = create_function("func2", vec![Decorator::After("func1".to_string())], "body2");
|
||||
let functions = vec![function1.clone(), function2.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
assert_eq!(result[1].name, "func2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_dependencies() {
|
||||
let function1 = create_function("func1", vec![], "body1");
|
||||
let function2 = create_function("func2", vec![Decorator::After("func1".to_string())], "body2");
|
||||
let function3 = create_function("func3", vec![Decorator::After("func2".to_string())], "body3");
|
||||
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
assert_eq!(result[1].name, "func2");
|
||||
assert_eq!(result[2].name, "func3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_dependencies() {
|
||||
let function1 = create_function("func1", vec![], "body1");
|
||||
let function2 = create_function("func2", vec![
|
||||
Decorator::After("func1".to_string()),
|
||||
Decorator::After("func3".to_string()),
|
||||
], "body2");
|
||||
let function3 = create_function("func3", vec![Decorator::After("func1".to_string())], "body3");
|
||||
let function4 = create_function("func4", vec![], "body4");
|
||||
let functions = vec![function1.clone(), function2.clone(), function3.clone(), function4.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
|
||||
let positions: std::collections::HashMap<_, _> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (&f.name, i))
|
||||
.collect();
|
||||
|
||||
// func1 应该在 func2 和 func3 之前
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func2".to_string()]);
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||
|
||||
// func3 应该在 func2 之前
|
||||
assert!(positions[&"func3".to_string()] < positions[&"func2".to_string()]);
|
||||
|
||||
// func4 可以在任何位置,因为没有依赖关系
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_decorators() {
|
||||
let function1 = create_function("func1", vec![
|
||||
Decorator::Pipeline,
|
||||
Decorator::Timeout(1000),
|
||||
], "body1");
|
||||
let function2 = create_function("func2", vec![
|
||||
Decorator::After("func1".to_string()),
|
||||
Decorator::Retry(3, 100),
|
||||
Decorator::Fallible,
|
||||
], "body2");
|
||||
let functions = vec![function1.clone(), function2.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].name, "func1");
|
||||
assert_eq!(result[1].name, "func2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_multiple_after_decorators() {
|
||||
let function1 = create_function("func1", vec![], "body1");
|
||||
let function2 = create_function("func2", vec![], "body2");
|
||||
let function3 = create_function("func3", vec![
|
||||
Decorator::After("func1".to_string()),
|
||||
Decorator::After("func2".to_string()),
|
||||
], "body3");
|
||||
let functions = vec![function1.clone(), function2.clone(), function3.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
|
||||
let positions: std::collections::HashMap<_, _> = result
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (&f.name, i))
|
||||
.collect();
|
||||
|
||||
// func1 和 func2 都应该在 func3 之前
|
||||
assert!(positions[&"func1".to_string()] < positions[&"func3".to_string()]);
|
||||
assert!(positions[&"func2".to_string()] < positions[&"func3".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignores_non_after_decorators() {
|
||||
let function1 = create_function("func1", vec![
|
||||
Decorator::Pipeline,
|
||||
Decorator::If("condition".to_string()),
|
||||
Decorator::Fallible,
|
||||
], "body1");
|
||||
let function2 = create_function("func2", vec![
|
||||
Decorator::Loop(5),
|
||||
Decorator::Timeout(1000),
|
||||
Decorator::Retry(3, 100),
|
||||
Decorator::Export,
|
||||
], "body2");
|
||||
let functions = vec![function1.clone(), function2.clone()];
|
||||
|
||||
let result = sort_function(functions);
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
// 由于没有 After 装饰器,两个函数都应该在结果中,顺序不重要
|
||||
let names: Vec<String> = result.iter().map(|f| f.name.clone()).collect();
|
||||
assert!(names.contains(&"func1".to_string()));
|
||||
assert!(names.contains(&"func2".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use std::collections::HashSet;
|
||||
use regex::Regex;
|
||||
pub struct Variable{
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref VAR_REGEX: Regex = Regex::new(r#"\$(?:\{(?:#)?([a-zA-Z_][a-zA-Z0-9_]*)(?:\[(?:\d+|@|\*)?\])?(?:\s*:\s*[+-]?\d+(?::[+-]?\d+)?)?\}|\b([a-zA-Z_][a-zA-Z0-9_]*)\b)"#)
|
||||
.expect("Failed to compile variable regex");
|
||||
}
|
||||
|
||||
pub fn extract_variables(body: String) -> Vec<Variable> {
|
||||
let mut variables = HashSet::<String>::new();
|
||||
let mut variable_list = Vec::<Variable>::new();
|
||||
|
||||
for captures in VAR_REGEX.captures_iter(&body) {
|
||||
if let Some(name) = captures.get(1) {
|
||||
dbg!(name);
|
||||
variables.insert(name.as_str().to_string());
|
||||
} else if let Some(name) = captures.get(2) {
|
||||
dbg!(name);
|
||||
variables.insert(name.as_str().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
for variable in variables {
|
||||
let variable_element = Variable{
|
||||
name: variable.clone(),
|
||||
value: Default::default(), // TODO: 换成workshop-agent通信
|
||||
};
|
||||
variable_list.push(variable_element);
|
||||
}
|
||||
|
||||
variable_list
|
||||
}
|
||||
|
||||
// 单元测试模块
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// 辅助函数:将Variable列表转换为排序后的字符串集合(解决HashSet无序问题)
|
||||
fn sorted_var_names(vars: &[Variable]) -> BTreeSet<String> {
|
||||
vars.iter()
|
||||
.map(|v| v.name.clone())
|
||||
.collect::<BTreeSet<_>>()
|
||||
}
|
||||
|
||||
/// 测试1:基础场景(重复变量去重 + 多种合法格式)
|
||||
#[test]
|
||||
fn test_extract_duplicate_variables() {
|
||||
let script = r#"
|
||||
echo $var
|
||||
echo ${var}
|
||||
echo ${#var}
|
||||
echo ${var:3:5}
|
||||
echo ${var: -3}
|
||||
echo ${var[0]}
|
||||
echo ${var[@]}
|
||||
echo ${var[*]}
|
||||
echo $_var123
|
||||
echo ${_var123: -5:10}
|
||||
"#;
|
||||
|
||||
let variables = extract_variables(script.to_string());
|
||||
let sorted_names = sorted_var_names(&variables);
|
||||
|
||||
// 预期结果:去重后仅保留var和_var123
|
||||
let expected = BTreeSet::from(["var".to_string(), "_var123".to_string()]);
|
||||
assert_eq!(sorted_names, expected);
|
||||
}
|
||||
|
||||
/// 测试2:非法变量(数字开头、特殊字符、$1/$?等)
|
||||
#[test]
|
||||
fn test_extract_illegal_variables() {
|
||||
let script = r#"
|
||||
echo $1 # 数字开头,不匹配
|
||||
echo $? # 特殊符号,不匹配
|
||||
echo $@ # 特殊符号,不匹配
|
||||
echo ${123var} # 数字开头,不匹配
|
||||
echo ${var-with-dash} # 含非法字符,不匹配
|
||||
echo ${var+123} # 含非法字符,不匹配
|
||||
echo ${} # 空变量,不匹配
|
||||
echo $ # 无变量名,不匹配
|
||||
"#;
|
||||
|
||||
let variables = extract_variables(script.to_string());
|
||||
assert!(variables.is_empty());
|
||||
}
|
||||
|
||||
/// 测试3:空输入
|
||||
#[test]
|
||||
fn test_extract_empty_body() {
|
||||
let variables = extract_variables("".to_string());
|
||||
assert!(variables.is_empty());
|
||||
}
|
||||
|
||||
/// 测试4:无合法变量的输入
|
||||
#[test]
|
||||
fn test_extract_no_valid_variables() {
|
||||
let script = r#"
|
||||
echo "hello world"
|
||||
cd /tmp
|
||||
ls -l
|
||||
echo $123abc # 数字开头,不匹配
|
||||
"#;
|
||||
|
||||
let variables = extract_variables(script.to_string());
|
||||
assert!(variables.is_empty());
|
||||
}
|
||||
|
||||
/// 测试5:混合合法/非法变量(仅保留合法项)
|
||||
#[test]
|
||||
fn test_extract_mixed_variables() {
|
||||
let script = r#"
|
||||
echo $APP_NAME
|
||||
echo ${DEPLOY_ENV}
|
||||
echo ${#PORT}
|
||||
echo $1 # 非法
|
||||
echo ${invalid-var} # 非法
|
||||
echo ${_DB_PASSWORD: 5}
|
||||
echo $? # 非法
|
||||
"#;
|
||||
|
||||
let variables = extract_variables(script.to_string());
|
||||
let sorted_names = sorted_var_names(&variables);
|
||||
|
||||
let expected = BTreeSet::from([
|
||||
"APP_NAME".to_string(),
|
||||
"DEPLOY_ENV".to_string(),
|
||||
"PORT".to_string(),
|
||||
"_DB_PASSWORD".to_string(),
|
||||
]);
|
||||
assert_eq!(sorted_names, expected);
|
||||
}
|
||||
|
||||
/// 测试6:变量名包含数字(合法场景)
|
||||
#[test]
|
||||
fn test_extract_var_with_numbers() {
|
||||
let script = r#"
|
||||
echo $var123
|
||||
echo ${var_456}
|
||||
echo ${#var789: 10}
|
||||
"#;
|
||||
|
||||
let variables = extract_variables(script.to_string());
|
||||
let sorted_names = sorted_var_names(&variables);
|
||||
|
||||
let expected = BTreeSet::from([
|
||||
"var123".to_string(),
|
||||
"var_456".to_string(),
|
||||
"var789".to_string(),
|
||||
]);
|
||||
assert_eq!(sorted_names, expected);
|
||||
}
|
||||
}
|
||||
Generated
+169
@@ -0,0 +1,169 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "if-addrs"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "macaddr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "route-rs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66fa49dc79c685a59fa33339e126a794a67cf4ed865c2c9785268e23ec51409"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "workshop-helper-mac"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"if-addrs",
|
||||
"macaddr",
|
||||
"route-rs",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "workshop-helper-mac"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
if-addrs = "0.14.0"
|
||||
macaddr = "1.0.1"
|
||||
route-rs = "0.1.0"
|
||||
thiserror = "2.0.17"
|
||||
@@ -0,0 +1,69 @@
|
||||
use if_addrs::IfAddr;
|
||||
use macaddr::MacAddr;
|
||||
use std::net::IpAddr;
|
||||
use thiserror::Error;
|
||||
|
||||
// 定义一个自定义的错误类型,让错误处理更优雅
|
||||
#[derive(Error, Debug)]
|
||||
enum MainMacError {
|
||||
#[error("无法获取网络接口列表: {0}")]
|
||||
IfAddrsError(#[from] if_addrs::Error),
|
||||
|
||||
#[error("无法获取路由信息: {0}")]
|
||||
RouteError(#[from] route_rs::Error),
|
||||
|
||||
#[error("未找到默认路由")]
|
||||
NoDefaultRoute,
|
||||
|
||||
#[error("默认路由对应的接口 '{0}' 未在接口列表中找到")]
|
||||
InterfaceNotFound(String),
|
||||
|
||||
#[error("接口 '{0}' 没有 MAC 地址")]
|
||||
NoMacAddress(String),
|
||||
|
||||
#[error("找到的主网卡 '{0}' 是虚拟接口,已跳过")]
|
||||
VirtualInterface(String),
|
||||
}
|
||||
|
||||
/// 检查接口名称是否暗示它是一个虚拟接口
|
||||
fn is_virtual_interface(name: &str) -> bool {
|
||||
let virtual_patterns = ["tun", "tap", "virbr", "veth", "lo"];
|
||||
virtual_patterns.iter().any(|&pat| name.contains(pat))
|
||||
}
|
||||
|
||||
/// 寻找并返回主网卡的 MAC 地址
|
||||
fn find_main_mac_address() -> Result<MacAddr, MainMacError> {
|
||||
// 步骤 1: 获取默认路由,以确定主网卡的名称
|
||||
let default_route = route_rs::get_default_route()?;
|
||||
let iface_name = default_route.interface_name().ok_or(MainMacError::NoDefaultRoute)?;
|
||||
println!("默认路由所在的接口名称: '{}'", iface_name);
|
||||
|
||||
// 步骤 2: 检查是否为虚拟接口
|
||||
if is_virtual_interface(iface_name) {
|
||||
return Err(MainMacError::VirtualInterface(iface_name.to_string()));
|
||||
}
|
||||
|
||||
// 步骤 3: 获取所有网络接口的详细信息
|
||||
let if_addrs = if_addrs::get_if_addrs()?;
|
||||
|
||||
// 步骤 4: 遍历接口列表,找到与默认路由接口名匹配的接口
|
||||
for if_addr in if_addrs {
|
||||
if if_addr.name == iface_name {
|
||||
// 找到了匹配的接口,提取其 MAC 地址
|
||||
return if_addr.mac_addr.ok_or_else(|| MainMacError::NoMacAddress(iface_name.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果循环结束都没有找到,说明出了问题
|
||||
Err(MainMacError::InterfaceNotFound(iface_name.to_string()))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("正在寻找主网卡的 MAC 地址...");
|
||||
|
||||
match find_main_mac_address() {
|
||||
Ok(mac) => println!("成功找到主网卡 MAC 地址: {}", mac),
|
||||
Err(e) => eprintln!("失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,9 @@
|
||||
#!/bin/bash
|
||||
# .workshop/bake
|
||||
# .workshop/bake.sh
|
||||
# 构建阶段脚本 - 使用标准的bash语法
|
||||
|
||||
set -euo pipefail # 严格模式:错误退出、未定义变量检查、管道错误检查
|
||||
|
||||
# 颜色定义用于输出
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
||||
}
|
||||
|
||||
# 环境变量(由CI/CD系统注入)
|
||||
readonly WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}"
|
||||
readonly BUILD_ID="${BUILD_ID:-unknown}"
|
||||
readonly COMMIT_SHA="${COMMIT_SHA:-}"
|
||||
readonly BRANCH_NAME="${BRANCH_NAME:-main}"
|
||||
readonly BUILD_CACHE_DIR="${BUILD_CACHE_DIR:-/cache}"
|
||||
|
||||
# 构建配置(从prebake和环境变量读取)
|
||||
readonly RUST_VERSION="${RUST_VERSION:-1.70}"
|
||||
readonly BUILD_TYPE="${BUILD_TYPE:-release}"
|
||||
readonly CARGO_REGISTRY="${CARGO_REGISTRY:-$BUILD_CACHE_DIR/cargo/registry}"
|
||||
readonly CARGO_GIT="${CARGO_GIT:-$BUILD_CACHE_DIR/cargo/git}"
|
||||
|
||||
# 切换到工作目录
|
||||
cd "$WORKSPACE_DIR"
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 颜色定义用于输出
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly NC='\033[0m' # 无颜色
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*${NC}"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*${NC}"
|
||||
}
|
||||
|
||||
# 读取环境变量
|
||||
source .env
|
||||
# 执行bake步骤
|
||||
source bake.sh
|
||||
@@ -1,4 +1,4 @@
|
||||
# .workshop/finalize.yaml
|
||||
# finalize.yaml.tmpl
|
||||
# 版本号
|
||||
version: "1.0"
|
||||
|
||||
@@ -112,27 +112,6 @@ distribution:
|
||||
# 质量门禁
|
||||
quality_gates:
|
||||
# 测试覆盖率要求
|
||||
test_coverage:
|
||||
enabled: true
|
||||
minimum: 80.0
|
||||
file: "coverage/lcov.info"
|
||||
|
||||
# 安全扫描
|
||||
security_scan:
|
||||
enabled: true
|
||||
tools:
|
||||
- "cargo-audit"
|
||||
- "trivy"
|
||||
fail_on: "high" # low, medium, high, critical
|
||||
exceptions:
|
||||
- "CVE-2021-12345" # 已知例外
|
||||
|
||||
# 代码质量
|
||||
code_quality:
|
||||
enabled: true
|
||||
maximum_duplication: 5.0
|
||||
maximum_complexity: 25.0
|
||||
minimum_maintainability: 50.0
|
||||
|
||||
# 部署配置
|
||||
deployment:
|
||||
@@ -156,6 +135,8 @@ deployment:
|
||||
cluster: "production-cluster"
|
||||
manual_approval: true
|
||||
rollout_strategy: "blue-green"
|
||||
manifests:
|
||||
- "k8s/deployment.yaml"
|
||||
|
||||
# 部署策略
|
||||
strategy:
|
||||
@@ -177,6 +158,7 @@ notifications:
|
||||
to:
|
||||
- "team@example.com"
|
||||
subject: "Build Success: {{.project_name}} v{{.version}}"
|
||||
message: "Build {{.build_id}} succeeded for {{.project_name}} v{{.version}}"
|
||||
template: "success_email.html"
|
||||
|
||||
# 失败通知
|
||||
@@ -188,6 +170,7 @@ notifications:
|
||||
|
||||
- type: "pagerduty"
|
||||
service_key: "{{env.PAGERDUTY_KEY}}"
|
||||
message: "Build {{.build_id}} failed for {{.project_name}} v{{.version}}"
|
||||
severity: "error"
|
||||
|
||||
# 部署通知
|
||||
@@ -197,6 +180,7 @@ notifications:
|
||||
method: "POST"
|
||||
headers:
|
||||
Authorization: "Bearer {{env.DEPLOY_WEBHOOK_TOKEN}}"
|
||||
message: ""
|
||||
|
||||
# 清理策略
|
||||
cleanup:
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
# .workshop/prebake.yaml
|
||||
# prebake.yaml.tmpl
|
||||
#
|
||||
# 术语说明:
|
||||
# 可选:该字段可以不给出
|
||||
# 默认为...:隐含“可选”
|
||||
# 留空:尚未定义,留作后续
|
||||
|
||||
# 版本号,用于格式兼容性检查
|
||||
version: "1.0"
|
||||
|
||||
@@ -7,59 +13,57 @@ environment:
|
||||
# 构建机类型
|
||||
builder: "docker" # 或 "firecracker", "custom", "baremetal"
|
||||
|
||||
# 构建机配置
|
||||
# 构建机配置,注意以下只会出现与上述构建机类型对应的组。
|
||||
config:
|
||||
baremetal:
|
||||
memory_mb: 2048
|
||||
cpus: 2
|
||||
arch: x86_64
|
||||
# baremetal:
|
||||
# 不需要
|
||||
|
||||
# Docker 构建机配置
|
||||
docker:
|
||||
base_image: "rust:1.70-slim"
|
||||
dockerfile: |
|
||||
FROM rust:1.70-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y pkg-config libssl-dev
|
||||
# docker:
|
||||
# 镜像名
|
||||
image: "rust:1.70-slim"
|
||||
# Dockerfile路径,相对于项目根目录,不能与镜像一同指定,两者必须指定其一
|
||||
dockerfile: "/Dockerfile"
|
||||
# Docker构建参数,可选,只能与dockerfile搭配使用
|
||||
build_args:
|
||||
- "RUST_VERSION=1.70"
|
||||
|
||||
# Firecracker 构建机配置
|
||||
firecracker:
|
||||
kernel_image: "file:///opt/firecracker/vmlinux"
|
||||
rootfs_image: "file:///opt/firecracker/rootfs.ext4"
|
||||
memory_mb: 1024
|
||||
vcpus: 2
|
||||
boot_args: "console=ttyS0 reboot=k panic=1 pci=off"
|
||||
# firecracker:
|
||||
# 留空
|
||||
|
||||
# 自定义构建机配置
|
||||
custom:
|
||||
# custom:
|
||||
# 自定义构建机名称
|
||||
name: "my-custom-builder"
|
||||
# 设置脚本,相对于项目根目录
|
||||
setup_script: "setup-builder.sh"
|
||||
# 清洁脚本,相对于项目根目录
|
||||
cleanup_script: "cleanup-builder.sh"
|
||||
|
||||
# 硬件资源要求
|
||||
# 硬件资源要求,满足最小要求的构建机可以被选中
|
||||
resources:
|
||||
cpu: 2 # CPU核心数
|
||||
memory: "2G" # 内存大小
|
||||
disk: "10G" # 磁盘空间
|
||||
architecture: "x86_64" # 架构要求
|
||||
gpu: false # 是否需要GPU
|
||||
gpu_type: "cuda" # GPU类型(如果需要)
|
||||
cpu: 2 # CPU核心数,默认为1
|
||||
memory: "2G" # 内存大小,也可以写作2048MB,默认为"1G"
|
||||
disk: "10G" # 磁盘空间,也可以写作10240MB,默认为"1G"
|
||||
architecture: "x86_64" # 架构要求,只能是"noarch"或{"x86_64"/"amd64", "arm64"/"aarch64", "riscv64", "loongarch64"/"loong64"}中的一个或几个
|
||||
gpu: false # 是否需要GPU,默认为false
|
||||
gpu_type: "cuda" # GPU类型(如果需要),可以是"cuda", "rocm", "vulkan", "oneapi"中的一个或几个
|
||||
tags: # 自定义标签,只有满足标签的构建机可以被选中
|
||||
- "custom_tag"
|
||||
|
||||
# 网络配置
|
||||
network:
|
||||
enabled: true
|
||||
outbound: true # 允许出站连接
|
||||
dns_servers:
|
||||
enabled: true # 启用网络,对baremetal不适用,默认为true
|
||||
outbound: true # 允许出站连接,对baremetal不适用,默认为true
|
||||
dns_servers: # 指定构建机的DNS服务器,对baremetal不适用,custom应该自行处理DNS问题,可选
|
||||
- "8.8.8.8"
|
||||
- "1.1.1.1"
|
||||
proxies: # 代理配置,暂时留空,可选
|
||||
|
||||
# 依赖定义
|
||||
# 依赖定义,安装依赖时按custom_pre-system-languages-custom顺序进行
|
||||
dependencies:
|
||||
# 系统包依赖
|
||||
# 系统包依赖,按软件包类型给出或any通配,若存在发行版则选择之,否则选中any,也没有则报警告但继续
|
||||
system:
|
||||
packages:
|
||||
deb:
|
||||
packages: # 安装的软件包
|
||||
- "git"
|
||||
- "curl"
|
||||
- "build-essential"
|
||||
@@ -67,34 +71,39 @@ dependencies:
|
||||
- "libssl-dev"
|
||||
repositories:
|
||||
- "ppa:custom/ppa" # 自定义PPA(Ubuntu)
|
||||
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
|
||||
pacman:
|
||||
packages:
|
||||
- "git"
|
||||
- "curl"
|
||||
- "base-devel"
|
||||
- "pkg-config"
|
||||
- "openssl"
|
||||
repositories:
|
||||
- "https://mirrors.tuna.tsinghua.edu.cn/arch4edu/$arch" # 自定义仓库(Arch)
|
||||
mirror: "https://mirrors.tuna.tsinghua.edu.cn" # 指定软件包镜像源,可选
|
||||
rpm:
|
||||
# 略
|
||||
|
||||
# 语言特定依赖
|
||||
# 语言特定依赖,可选,此处定义不稳定
|
||||
languages:
|
||||
rust:
|
||||
version: "1.70"
|
||||
toolchain: "stable"
|
||||
components:
|
||||
- "rustfmt"
|
||||
- "clippy"
|
||||
targets:
|
||||
- "x86_64-unknown-linux-gnu"
|
||||
toolchain: "stable-x86_64-unknown-linux-gnu"
|
||||
|
||||
node:
|
||||
version: "18"
|
||||
package_manager: "npm" # 或 "yarn", "pnpm"
|
||||
registry: "https://registry.npmjs.org/"
|
||||
|
||||
python:
|
||||
version: "3.11"
|
||||
package_manager: "pip" # 或 "poetry", "conda"
|
||||
requirements_file: "requirements.txt"
|
||||
|
||||
go:
|
||||
version: "1.19"
|
||||
modules: true
|
||||
|
||||
# 自定义依赖步骤
|
||||
# 自定义依赖步骤,可选
|
||||
custom:
|
||||
- name: "install-custom-tool" # 名称
|
||||
command: "curl -fsSL https://example.com/install.sh | sh" # 命令
|
||||
environment: # 环境变量
|
||||
CUSTOM_VAR: "value"
|
||||
|
||||
- name: "build-native-dep"
|
||||
command: "make && make install"
|
||||
working_dir: "native-deps" # 工作目录,基于项目根目录
|
||||
timeout: 300 # 超时,默认为300秒
|
||||
|
||||
# 自定义依赖步骤,但是在依赖安装时最先进行,可选
|
||||
custom_pre:
|
||||
- name: "install-custom-tool"
|
||||
command: "curl -fsSL https://example.com/install.sh | sh"
|
||||
environment:
|
||||
@@ -107,32 +116,33 @@ dependencies:
|
||||
|
||||
# 环境变量配置
|
||||
environment_variables:
|
||||
# 构建环境变量
|
||||
build:
|
||||
# 构建环境变量,只在prebake阶段有效
|
||||
prebake:
|
||||
RUST_BACKTRACE: "full"
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
NODE_ENV: "production"
|
||||
|
||||
# 运行时环境变量(会传递给bake阶段)
|
||||
runtime:
|
||||
# 运行时环境变量(会传递给bake阶段,与该阶段获取的环境一并作为.env保存)
|
||||
bake:
|
||||
DATABASE_URL: "postgresql://user:pass@localhost/db"
|
||||
LOG_LEVEL: "info"
|
||||
__ENV_FILE: ".env" # 特殊变量,指定项目中.env的位置并合并其中的值。该值本身不传入bake阶段
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
# 缓存目录
|
||||
directories:
|
||||
- path: "/usr/local/cargo"
|
||||
strategy: "persistent" # persistent, build, session
|
||||
compression: "zstd"
|
||||
strategy: "always" # 在"always","readonly","clean"和"never"中选择一个,可以被流水线启动时配置覆盖,默认为"always"
|
||||
mode: "zstd" # 在"gzip","zstd","none"和"dir"中选择一个
|
||||
|
||||
- path: "/root/.npm"
|
||||
strategy: "build"
|
||||
compression: "none"
|
||||
strategy: "clean"
|
||||
mode: "none"
|
||||
|
||||
- path: "/app/target"
|
||||
strategy: "persistent"
|
||||
compression: "zstd"
|
||||
strategy: "always"
|
||||
mode: "dir"
|
||||
|
||||
# 缓存策略
|
||||
strategy:
|
||||
@@ -142,20 +152,7 @@ cache:
|
||||
|
||||
# 安全配置
|
||||
security:
|
||||
# 权限限制
|
||||
capabilities:
|
||||
- "NET_BIND_SERVICE"
|
||||
|
||||
# 安全策略
|
||||
seccomp: "strict" # strict, filter, disabled
|
||||
apparmor: true
|
||||
no_new_privileges: true
|
||||
|
||||
# 资源限制
|
||||
limits:
|
||||
max_processes: 512
|
||||
max_file_size: "1G"
|
||||
max_open_files: 1024
|
||||
# 留空
|
||||
|
||||
# 钩子脚本
|
||||
hooks:
|
||||
|
||||
@@ -5,75 +5,3 @@ pub mod scripts;
|
||||
pub use prebake::*;
|
||||
pub use finalize::*;
|
||||
pub use scripts::*;
|
||||
// use log::info;
|
||||
|
||||
// use crate::TemplateResolvable;
|
||||
|
||||
// 完整的流水线配置
|
||||
// #[derive(Debug, Clone, serde::Serialize)]
|
||||
// pub struct PipelineConfig {
|
||||
// pub prebake: PrebakeConfig,
|
||||
// pub bake: BakeScript,
|
||||
// pub finalize: FinalizeConfig,
|
||||
// pub variables: std::collections::HashMap<String, crate::variable::VariableValue>,
|
||||
// }
|
||||
|
||||
// impl PipelineConfig {
|
||||
// /// 从文件系统加载完整配置
|
||||
// pub fn load_from_dir<P: AsRef<std::path::Path>>(
|
||||
// dir: P,
|
||||
// user_id: &str,
|
||||
// pipeline_id: &str,
|
||||
// variable_provider: &dyn crate::variable::VariableProvider,
|
||||
// ) -> crate::error::PipelineResult<Self> {
|
||||
// let dir = dir.as_ref();
|
||||
|
||||
// info!("Reading prebake.yaml...");
|
||||
// let prebake_path = dir.join(".workshop/prebake.yaml");
|
||||
// info!("Reading bake.sh...");
|
||||
// let bake_path = dir.join(".workshop/bake.sh");
|
||||
// info!("Reading finalize.yaml...");
|
||||
// let finalize_path = dir.join(".workshop/finalize.yaml");
|
||||
|
||||
// let mut variable_resolver = crate::variable::VariableResolver::new(
|
||||
// user_id,
|
||||
// pipeline_id,
|
||||
// variable_provider
|
||||
// );
|
||||
|
||||
// // 加载并处理prebake配置
|
||||
// let prebake_content = std::fs::read_to_string(&prebake_path)?;
|
||||
// let prebake: PrebakeConfig = serde_yaml::from_str(&prebake_content)?;
|
||||
// // let prebake = variable_resolver.resolve_prebake(prebake)?;
|
||||
// let prebake = prebake.resolve_template(&mut variable_resolver)?;
|
||||
|
||||
// // 加载bake脚本
|
||||
// let bake_content = std::fs::read_to_string(&bake_path)?;
|
||||
// let bake = BakeScript::new(bake_content);
|
||||
|
||||
// // 加载并处理finalize配置
|
||||
// let finalize_content = std::fs::read_to_string(&finalize_path)?;
|
||||
// let finalize: FinalizeConfig = serde_yaml::from_str(&finalize_content)?;
|
||||
// // let finalize = variable_resolver.resolve_finalize(finalize)?;
|
||||
// let finalize = finalize.resolve_template(&mut variable_resolver)?;
|
||||
|
||||
// Ok(Self {
|
||||
// prebake,
|
||||
// bake,
|
||||
// finalize,
|
||||
// variables: variable_resolver.get_resolved_variables(),
|
||||
// })
|
||||
// }
|
||||
|
||||
// /// 获取用于输出的配置(遮蔽secret)
|
||||
// pub fn for_output(&self) -> Self {
|
||||
// Self {
|
||||
// prebake: self.prebake.clone(),
|
||||
// bake: self.bake.clone(),
|
||||
// finalize: self.finalize.for_output(),
|
||||
// variables: self.variables.iter()
|
||||
// .map(|(k, v)| (k.clone(), v.for_output()))
|
||||
// .collect(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -3,6 +3,7 @@ mod dependencies;
|
||||
mod security;
|
||||
mod hooks;
|
||||
|
||||
use std::collections::HashMap;
|
||||
pub use environment::*;
|
||||
pub use dependencies::*;
|
||||
pub use security::*;
|
||||
@@ -23,26 +24,54 @@ pub struct PrebakeConfig {
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
/// 环境变量配置
|
||||
/// 环境变量配置 - 调整阶段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentVariables {
|
||||
pub build: Option<std::collections::HashMap<String, String>>,
|
||||
pub runtime: Option<std::collections::HashMap<String, String>>,
|
||||
pub prebake: Option<HashMap<String, String>>,
|
||||
pub bake: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 缓存配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheConfig {
|
||||
pub directories: Vec<CacheDirectory>,
|
||||
pub strategy: CacheStrategy,
|
||||
pub strategy: Option<CacheStrategy>,
|
||||
}
|
||||
|
||||
/// 缓存目录配置
|
||||
/// 缓存目录配置 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheDirectory {
|
||||
pub path: String,
|
||||
pub strategy: String,
|
||||
pub compression: String,
|
||||
#[serde(default = "default_cache_strategy")]
|
||||
pub strategy: CacheStrategyType,
|
||||
#[serde(default = "default_cache_mode")]
|
||||
pub mode: CacheMode,
|
||||
}
|
||||
|
||||
/// 缓存策略类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CacheStrategyType {
|
||||
#[serde(rename = "always")]
|
||||
Always,
|
||||
#[serde(rename = "readonly")]
|
||||
ReadOnly,
|
||||
#[serde(rename = "clean")]
|
||||
Clean,
|
||||
#[serde(rename = "never")]
|
||||
Never,
|
||||
}
|
||||
|
||||
/// 缓存模式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum CacheMode {
|
||||
#[serde(rename = "gzip")]
|
||||
Gzip,
|
||||
#[serde(rename = "zstd")]
|
||||
Zstd,
|
||||
#[serde(rename = "none")]
|
||||
None,
|
||||
#[serde(rename = "dir")]
|
||||
Dir,
|
||||
}
|
||||
|
||||
/// 缓存策略
|
||||
@@ -53,6 +82,12 @@ pub struct CacheStrategy {
|
||||
pub cleanup_policy: String,
|
||||
}
|
||||
|
||||
/// 安全配置 - 留空
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityConfig {
|
||||
// 根据新模板,暂时留空
|
||||
}
|
||||
|
||||
/// 元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
@@ -61,3 +96,12 @@ pub struct Metadata {
|
||||
pub tags: Vec<String>,
|
||||
pub project_type: String,
|
||||
}
|
||||
|
||||
// 默认值函数
|
||||
fn default_cache_strategy() -> CacheStrategyType {
|
||||
CacheStrategyType::Always
|
||||
}
|
||||
|
||||
fn default_cache_mode() -> CacheMode {
|
||||
CacheMode::Zstd
|
||||
}
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 依赖配置
|
||||
/// 依赖配置 - 调整顺序
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DependenciesConfig {
|
||||
pub custom_pre: Option<Vec<CustomDependency>>,
|
||||
pub system: Option<SystemDependencies>,
|
||||
pub languages: Option<LanguageDependencies>,
|
||||
pub custom: Option<Vec<CustomDependency>>,
|
||||
}
|
||||
|
||||
/// 系统依赖
|
||||
/// 系统依赖 - 按发行版组织
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SystemDependencies {
|
||||
pub packages: Vec<String>,
|
||||
pub repositories: Vec<String>,
|
||||
pub ubuntu: Option<DistributionDeps>,
|
||||
pub arch: Option<DistributionDeps>,
|
||||
pub fedora: Option<DistributionDeps>,
|
||||
// 可以添加其他发行版...
|
||||
}
|
||||
|
||||
/// 语言依赖
|
||||
/// 发行版依赖
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DistributionDeps {
|
||||
pub packages: Vec<String>,
|
||||
pub repositories: Option<Vec<String>>,
|
||||
pub mirror: Option<String>,
|
||||
}
|
||||
|
||||
/// 语言依赖 - 简化结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageDependencies {
|
||||
pub rust: Option<RustConfig>,
|
||||
// 其他语言暂时保持不变...
|
||||
pub node: Option<NodeConfig>,
|
||||
pub python: Option<PythonConfig>,
|
||||
pub go: Option<GoConfig>,
|
||||
}
|
||||
|
||||
/// Rust配置
|
||||
/// Rust配置 - 简化
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RustConfig {
|
||||
pub version: String,
|
||||
pub toolchain: String,
|
||||
pub components: Vec<String>,
|
||||
pub targets: Vec<String>,
|
||||
}
|
||||
|
||||
/// Node配置
|
||||
@@ -56,12 +66,17 @@ pub struct GoConfig {
|
||||
pub modules: bool,
|
||||
}
|
||||
|
||||
// TODO: 更多语言配置
|
||||
|
||||
/// 自定义依赖
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomDependency {
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub environment: Option<std::collections::HashMap<String, String>>,
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
pub working_dir: Option<String>,
|
||||
pub timeout: Option<u32>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u32,
|
||||
}
|
||||
|
||||
fn default_timeout() -> u32 { 300 }
|
||||
|
||||
@@ -9,7 +9,7 @@ pub struct EnvironmentConfig {
|
||||
pub network: NetworkConfig,
|
||||
}
|
||||
|
||||
/// 构建器配置
|
||||
/// 构建器配置 - 现在各个配置是互斥的
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuilderConfig {
|
||||
pub baremetal: Option<BaremetalConfig>,
|
||||
@@ -18,30 +18,27 @@ pub struct BuilderConfig {
|
||||
pub custom: Option<CustomBuilderConfig>,
|
||||
}
|
||||
|
||||
/// 裸机配置
|
||||
/// 裸机配置 - 现在不需要具体配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BaremetalConfig {
|
||||
pub memory_mb: u32,
|
||||
pub cpus: u32,
|
||||
pub arch: String,
|
||||
// 根据新模板,baremetal不需要配置项
|
||||
}
|
||||
|
||||
/// Docker配置
|
||||
/// Docker配置 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockerConfig {
|
||||
pub base_image: String,
|
||||
pub dockerfile: String,
|
||||
pub build_args: Vec<String>,
|
||||
/// 镜像名
|
||||
pub image: Option<String>,
|
||||
/// Dockerfile路径,相对于项目根目录
|
||||
pub dockerfile: Option<String>,
|
||||
/// Docker构建参数,可选,只能与dockerfile搭配使用
|
||||
pub build_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Firecracker配置
|
||||
/// Firecracker配置 - 留空
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FirecrackerConfig {
|
||||
pub kernel_image: String,
|
||||
pub rootfs_image: String,
|
||||
pub memory_mb: u32,
|
||||
pub vcpus: u32,
|
||||
pub boot_args: String,
|
||||
// 根据新模板,暂时留空
|
||||
}
|
||||
|
||||
/// 自定义构建器配置
|
||||
@@ -52,21 +49,69 @@ pub struct CustomBuilderConfig {
|
||||
pub cleanup_script: String,
|
||||
}
|
||||
|
||||
/// 资源需求
|
||||
/// 资源需求 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ResourceRequirements {
|
||||
#[serde(default = "default_cpu")]
|
||||
pub cpu: u32,
|
||||
#[serde(default = "default_memory")]
|
||||
pub memory: String,
|
||||
#[serde(default = "default_disk")]
|
||||
pub disk: String,
|
||||
pub architecture: String,
|
||||
pub architecture: Architecture,
|
||||
#[serde(default)]
|
||||
pub gpu: bool,
|
||||
pub gpu_type: Option<String>,
|
||||
pub gpu_type: Option<Vec<GpuType>>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 网络配置
|
||||
/// 架构要求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Architecture {
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
impl Default for Architecture {
|
||||
fn default() -> Self {
|
||||
Architecture::Single("x86_64".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// GPU类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GpuType {
|
||||
#[serde(rename = "cuda")]
|
||||
Cuda,
|
||||
#[serde(rename = "rocm")]
|
||||
Rocm,
|
||||
#[serde(rename = "vulkan")]
|
||||
Vulkan,
|
||||
#[serde(rename = "oneapi")]
|
||||
OneApi,
|
||||
}
|
||||
|
||||
/// 网络配置 - 更新字段
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkConfig {
|
||||
#[serde(default = "default_network_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_outbound")]
|
||||
pub outbound: bool,
|
||||
pub dns_servers: Vec<String>,
|
||||
pub dns_servers: Option<Vec<String>>,
|
||||
pub proxies: Option<ProxiesConfig>,
|
||||
}
|
||||
|
||||
/// 代理配置 - 暂时留空
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProxiesConfig {
|
||||
// 暂不定义具体字段
|
||||
}
|
||||
|
||||
// 默认值函数
|
||||
fn default_cpu() -> u32 { 1 }
|
||||
fn default_memory() -> String { "1G".to_string() }
|
||||
fn default_disk() -> String { "1G".to_string() }
|
||||
fn default_network_enabled() -> bool { true }
|
||||
fn default_outbound() -> bool { true }
|
||||
|
||||
+302
-129
@@ -1,15 +1,19 @@
|
||||
use crate::{
|
||||
config::prebake::PrebakeConfig,
|
||||
config::finalize::FinalizeConfig,
|
||||
variable::VariableResolver,
|
||||
config::prebake::{
|
||||
Architecture, CacheMode, CacheStrategyType, DistributionDeps, GpuType, PrebakeConfig,
|
||||
SystemDependencies,
|
||||
},
|
||||
error::PipelineError,
|
||||
variable::VariableResolver,
|
||||
};
|
||||
use regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
// use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
static ref HANDLEBARS_REGEX: Regex = Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}").unwrap();
|
||||
static ref HANDLEBARS_REGEX: Regex =
|
||||
Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}").unwrap();
|
||||
static ref SHELL_VAR_REGEX: Regex = Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").unwrap();
|
||||
}
|
||||
|
||||
@@ -19,6 +23,7 @@ pub trait TemplateResolvable {
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
// 为基本类型实现模板解析
|
||||
impl TemplateResolvable for String {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut result = self.clone();
|
||||
@@ -31,150 +36,318 @@ impl TemplateResolvable for String {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 $VARIABLE 格式
|
||||
// for cap in SHELL_VAR_REGEX.captures_iter(&result) {
|
||||
// let var_name = &cap[1];
|
||||
// if let Ok(value) = resolver.resolve_variable(var_name) {
|
||||
// result = result.replace(&cap[0], &value);
|
||||
// }
|
||||
// }
|
||||
// 处理$VARIABLE格式,TODO
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// 为Option<String>实现
|
||||
// impl TemplateResolvable for Option<String> {
|
||||
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
// match self {
|
||||
// Some(s) => Ok(Some(s.resolve_template(resolver)?)),
|
||||
// None => Ok(None),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 为Vec<String>实现
|
||||
impl TemplateResolvable for Vec<String> {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut result = Vec::new();
|
||||
for item in self {
|
||||
result.push(item.resolve_template(resolver)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// impl TemplateResolvable for Option<Vec<String>> {
|
||||
// fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
// match self {
|
||||
// Some(vec) => Ok(Some(vec.resolve_template(resolver)?)),
|
||||
// None => Ok(None),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// 为HashMap<String, String>实现
|
||||
impl TemplateResolvable for std::collections::HashMap<String, String> {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut result = std::collections::HashMap::new();
|
||||
for (key, value) in self {
|
||||
result.insert(
|
||||
key.resolve_template(resolver)?,
|
||||
value.resolve_template(resolver)?,
|
||||
);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TemplateResolvable> TemplateResolvable for Option<T> {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self {
|
||||
Some(value) => Ok(Some(value.resolve_template(resolver)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为Architecture实现
|
||||
impl TemplateResolvable for Architecture {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
match self {
|
||||
Architecture::Single(arch) => {
|
||||
Ok(Architecture::Single(arch.resolve_template(resolver)?))
|
||||
}
|
||||
Architecture::Multiple(archs) => {
|
||||
Ok(Architecture::Multiple(archs.resolve_template(resolver)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为SystemDependencies实现
|
||||
impl TemplateResolvable for SystemDependencies {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut deps = self.clone();
|
||||
|
||||
if let Some(ref mut ubuntu) = deps.ubuntu {
|
||||
*ubuntu = ubuntu.resolve_template(resolver)?;
|
||||
}
|
||||
if let Some(ref mut arch) = deps.arch {
|
||||
*arch = arch.resolve_template(resolver)?;
|
||||
}
|
||||
if let Some(ref mut fedora) = deps.fedora {
|
||||
*fedora = fedora.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
Ok(deps)
|
||||
}
|
||||
}
|
||||
|
||||
// 为DistributionDeps实现
|
||||
impl TemplateResolvable for DistributionDeps {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(DistributionDeps {
|
||||
packages: self.packages.resolve_template(resolver)?,
|
||||
repositories: self.repositories.resolve_template(resolver)?,
|
||||
mirror: self.mirror.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举类型不需要模板解析,直接克隆
|
||||
impl TemplateResolvable for GpuType {
|
||||
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateResolvable for CacheStrategyType {
|
||||
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateResolvable for CacheMode {
|
||||
fn resolve_template(&self, _resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// 为PrebakeConfig实现模板解析
|
||||
impl TemplateResolvable for PrebakeConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut config = self.clone();
|
||||
|
||||
// 解析环境配置
|
||||
config.environment = config.environment.resolve_template(resolver)?;
|
||||
|
||||
// 解析依赖配置
|
||||
if let Some(ref mut deps) = config.dependencies {
|
||||
*deps = deps.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
// 解析环境变量
|
||||
if let Some(ref mut env_vars) = config.environment_variables {
|
||||
if let Some(ref mut build) = env_vars.build {
|
||||
for (_key, value) in build.iter_mut() {
|
||||
*value = value.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
if let Some(ref mut runtime) = env_vars.runtime {
|
||||
for (_key, value) in runtime.iter_mut() {
|
||||
*value = value.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
*env_vars = env_vars.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
// 解析自定义依赖
|
||||
if let Some(ref mut deps) = config.dependencies {
|
||||
if let Some(ref mut custom) = deps.custom {
|
||||
for dep in custom.iter_mut() {
|
||||
dep.command = dep.command.resolve_template(resolver)?;
|
||||
if let Some(ref mut env) = dep.environment {
|
||||
for (_key, value) in env.iter_mut() {
|
||||
*value = value.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 解析缓存配置
|
||||
if let Some(ref mut cache) = config.cache {
|
||||
*cache = cache.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateResolvable for FinalizeConfig {
|
||||
// 为EnvironmentConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::EnvironmentConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
let mut config = self.clone();
|
||||
|
||||
// 解析产物路径
|
||||
if let Some(ref mut artifacts) = config.artifacts {
|
||||
if let Some(ref mut primary) = artifacts.primary {
|
||||
for artifact in primary.iter_mut() {
|
||||
artifact.path = artifact.path.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析打包配置
|
||||
if let Some(ref mut packaging) = config.packaging {
|
||||
if let Some(ref mut primary) = packaging.primary_package {
|
||||
primary.output = primary.output.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
if let Some(ref mut docker) = packaging.docker {
|
||||
docker.repository = docker.repository.resolve_template(resolver)?;
|
||||
for tag in docker.tags.iter_mut() {
|
||||
*tag = tag.resolve_template(resolver)?;
|
||||
}
|
||||
for build_arg in docker.build_args.iter_mut() {
|
||||
*build_arg = build_arg.resolve_template(resolver)?;
|
||||
}
|
||||
for (_key, value) in docker.labels.iter_mut() {
|
||||
*value = value.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析发布配置
|
||||
if let Some(ref mut distribution) = config.distribution {
|
||||
if let Some(ref mut local) = distribution.local {
|
||||
local.directory = local.directory.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
if let Some(ref mut s3) = distribution.s3 {
|
||||
s3.prefix = s3.prefix.resolve_template(resolver)?;
|
||||
}
|
||||
|
||||
if let Some(ref mut registries) = distribution.package_registries {
|
||||
if let Some(ref mut crates_io) = registries.crates_io {
|
||||
crates_io.token = crates_io.token.resolve_template(resolver)?;
|
||||
}
|
||||
if let Some(ref mut npm) = registries.npm {
|
||||
npm.token = npm.token.resolve_template(resolver)?;
|
||||
}
|
||||
if let Some(ref mut docker_registry) = registries.docker_registry {
|
||||
docker_registry.username = docker_registry.username.resolve_template(resolver)?;
|
||||
docker_registry.password = docker_registry.password.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析通知配置
|
||||
if let Some(ref mut notifications) = config.notifications {
|
||||
for notification in notifications.on_success.iter_mut() {
|
||||
notification.message = notification.message.resolve_template(resolver)?;
|
||||
if let Some(ref mut webhook) = notification.webhook {
|
||||
*webhook = webhook.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
|
||||
for notification in notifications.on_failure.iter_mut() {
|
||||
notification.message = notification.message.resolve_template(resolver)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl FinalizeConfig {
|
||||
pub fn for_output(&self) -> Self {
|
||||
let mut config = self.clone();
|
||||
|
||||
// 遮蔽secret类型的变量
|
||||
if let Some(ref mut distribution) = config.distribution {
|
||||
if let Some(ref mut registries) = distribution.package_registries {
|
||||
if let Some(ref mut crates_io) = registries.crates_io {
|
||||
crates_io.token = "***".to_string();
|
||||
}
|
||||
if let Some(ref mut npm) = registries.npm {
|
||||
npm.token = "***".to_string();
|
||||
}
|
||||
if let Some(ref mut docker_registry) = registries.docker_registry {
|
||||
docker_registry.username = "***".to_string();
|
||||
docker_registry.password = "***".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
Ok(Self {
|
||||
builder: self.builder.resolve_template(resolver)?,
|
||||
config: self.config.resolve_template(resolver)?,
|
||||
resources: self.resources.resolve_template(resolver)?,
|
||||
network: self.network.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为BuilderConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::BuilderConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
baremetal: self.baremetal.resolve_template(resolver)?,
|
||||
docker: self.docker.resolve_template(resolver)?,
|
||||
firecracker: self.firecracker.resolve_template(resolver)?,
|
||||
custom: self.custom.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为DockerConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::DockerConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
image: self.image.resolve_template(resolver)?,
|
||||
dockerfile: self.dockerfile.resolve_template(resolver)?,
|
||||
build_args: self.build_args.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为CustomBuilderConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::CustomBuilderConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
name: self.name.resolve_template(resolver)?,
|
||||
setup_script: self.setup_script.resolve_template(resolver)?,
|
||||
cleanup_script: self.cleanup_script.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为ResourceRequirements实现
|
||||
impl TemplateResolvable for crate::config::prebake::ResourceRequirements {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
cpu: self.cpu,
|
||||
memory: self.memory.resolve_template(resolver)?,
|
||||
disk: self.disk.resolve_template(resolver)?,
|
||||
architecture: self.architecture.resolve_template(resolver)?,
|
||||
gpu: self.gpu,
|
||||
gpu_type: self.gpu_type.clone(),
|
||||
tags: self.tags.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为NetworkConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::NetworkConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
enabled: self.enabled,
|
||||
outbound: self.outbound,
|
||||
dns_servers: self.dns_servers.resolve_template(resolver)?,
|
||||
proxies: self.proxies.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为DependenciesConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::DependenciesConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
custom_pre: self.custom_pre.resolve_template(resolver)?,
|
||||
system: self.system.resolve_template(resolver)?,
|
||||
languages: self.languages.resolve_template(resolver)?,
|
||||
custom: self.custom.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为LanguageDependencies实现
|
||||
impl TemplateResolvable for crate::config::prebake::LanguageDependencies {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
rust: self.rust.resolve_template(resolver)?,
|
||||
node: self.node.resolve_template(resolver)?,
|
||||
python: self.python.resolve_template(resolver)?,
|
||||
go: self.go.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为RustConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::RustConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
toolchain: self.toolchain.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为CustomDependency实现
|
||||
impl TemplateResolvable for crate::config::prebake::CustomDependency {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
name: self.name.resolve_template(resolver)?,
|
||||
command: self.command.resolve_template(resolver)?,
|
||||
environment: self.environment.resolve_template(resolver)?,
|
||||
working_dir: self.working_dir.resolve_template(resolver)?,
|
||||
timeout: self.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为EnvironmentVariables实现
|
||||
impl TemplateResolvable for crate::config::prebake::EnvironmentVariables {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
prebake: self.prebake.resolve_template(resolver)?,
|
||||
bake: self.bake.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为CacheConfig实现
|
||||
impl TemplateResolvable for crate::config::prebake::CacheConfig {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
directories: self.directories.resolve_template(resolver)?,
|
||||
strategy: self.strategy.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为CacheDirectory实现
|
||||
impl TemplateResolvable for crate::config::prebake::CacheDirectory {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
path: self.path.resolve_template(resolver)?,
|
||||
strategy: self.strategy.resolve_template(resolver)?,
|
||||
mode: self.mode.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 为CacheStrategy实现
|
||||
impl TemplateResolvable for crate::config::prebake::CacheStrategy {
|
||||
fn resolve_template(&self, resolver: &mut VariableResolver) -> Result<Self, PipelineError> {
|
||||
Ok(Self {
|
||||
ttl_days: self.ttl_days,
|
||||
max_size_gb: self.max_size_gb,
|
||||
cleanup_policy: self.cleanup_policy.resolve_template(resolver)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FinalizeConfig的模板解析保持不变...
|
||||
|
||||
Reference in New Issue
Block a user