refactor!: extract execution engine into standalone workshop-engine
crate BREAKING CHANGE: Moved engine/ module (cgroups, executor, pm, upm, types) and shared types (command, repology, time) from workshop-baker to new workshop-engine crate. All internal error types consolidated into workshop-engine::error. Import paths changed throughout codebase.
This commit is contained in:
-192
@@ -1,192 +0,0 @@
|
||||
# 2026-04-25 工作意图说明
|
||||
|
||||
## 工作范围
|
||||
|
||||
本次工作仅对 `workshop-baker` 进行操作,遵循以下原则:
|
||||
- 仅在**近期未更改或已稳定**的功能上操作
|
||||
- 小功能函数不受此限制
|
||||
- 不操作包含 `unimplemented!()`、`// TODO:` 或裸 `todo!()` 的函数/类型
|
||||
- 谨慎对待近期变更的代码
|
||||
|
||||
## 已完成工作
|
||||
|
||||
### 1. Docstring (`///`)
|
||||
|
||||
为以下稳定模块的所有 `pub` 函数/结构体/枚举添加了 docstring:
|
||||
|
||||
| 模块 | 文件数 | 说明 |
|
||||
|------|--------|------|
|
||||
| `types/` | 9 | architecture, time, compression, cache, command, builderconfig, repology, buildstatus, memsize(已有) |
|
||||
| `bake/` | 4 | decorator, schedule, parser, builder |
|
||||
| `engine/` | 5 | pm, pm/pacman, upm, cgroups, repology/local |
|
||||
| `finalize/` | 8 | fetch/checksum, fetch/types, fetch/extract, fetch/git, mail/config, mail/template, metadata, stage/hook, event |
|
||||
| `socket.rs` | 1 | get_socket_addr, establish_connection |
|
||||
|
||||
**跳过模块**:
|
||||
- `notify.rs` / `notify/types.rs` — 近期重构中,含 `todo!()`
|
||||
- `error.rs` / `lib.rs` / `main.rs` — 近期 CLI 重构
|
||||
- `finalize/config.rs` — 标记 "TODO: under heavy refactor"
|
||||
- `prebake/env/custom.rs` / `engine/pm/apt.rs` / `finalize/plugin/rhai.rs` — 含 `todo!()`
|
||||
- `daemon.rs` / `monitor/jobobject.rs` — 含 `todo!()` / `unimplemented!()`
|
||||
|
||||
### 2. 单元测试
|
||||
|
||||
新增 `#[cfg(test)] mod tests` 到以下文件:
|
||||
|
||||
| 文件 | 测试数 | 覆盖内容 |
|
||||
|------|--------|----------|
|
||||
| `types/architecture.rs` | 15 | normalized, as_str, FromStr 全变体+大小写+错误 |
|
||||
| `types/time.rs` | 11 | parse_duration, deserialize_duration, option_duration, 溢出/负值边界 |
|
||||
| `types/compression.rs` | 6 | Default, serde round-trip (使用 match 断言规避 PartialEq) |
|
||||
| `types/cache.rs` | 14 | default 值, serde round-trip |
|
||||
| `types/command.rs` | 3 | default 值, full/minimal serde |
|
||||
| `types/builderconfig.rs` | 5 | BuilderType serde, DockerConfig serde, untagged 行为 |
|
||||
| `types/repology.rs` | 10 | 全变体 deserialize, 空串→Default, unknown→error, serialize round-trip |
|
||||
| `engine/pm.rs` | 3 | all_managers 非空, select 已知/未知 |
|
||||
| `engine/upm.rs` | 3 | all_managers 非空, select 已知/未知 |
|
||||
| `engine/repology/local.rs` | 4 | resolve 已知包/未知包/多包/不同 PM |
|
||||
| `socket.rs` | 5 | get_socket_addr 全 5 种模式 |
|
||||
| `finalize/plugin/fetch/checksum.rs` | 9 | detect 长度分支, verify SHA256/SHA512 |
|
||||
| `finalize/plugin/fetch/types.rs` | 5 | get_checksum 组合, serde round-trip |
|
||||
| `finalize/plugin/fetch/extract.rs` | 8 | CompressionType::from_path 全扩展名 |
|
||||
| `finalize/plugin/fetch/git.rs` | 5 | parse_git_url 3 种模式+复杂 URL |
|
||||
| `finalize/plugin/internal/mail/config.rs` | 19 | EmailAddress, Recipients, MailConfig::validate, serde |
|
||||
| `finalize/plugin/internal/mail/template.rs` | 3 | render_template simple/build-status/invalid |
|
||||
| `finalize/plugin/metadata.rs` | 7 | default, clone, serde |
|
||||
|
||||
**已有测试未动**:`types/memsize.rs`, `bake/decorator.rs`, `bake/schedule.rs`, `types/buildstatus.rs`, `engine/pm/pacman.rs`
|
||||
|
||||
**测试修复记录**(非实现代码问题):
|
||||
- `types/builderconfig.rs` — untagged enum 测试中 DockerConfig 因全字段 optional+default 总是优先匹配,修正测试断言以反映实际 serde 行为
|
||||
- `types/command.rs` — serde roundtrip 中 `timeout` 字段 serialize 为秒、deserialize 时 `deserialize_duration_ms` 乘 1000,修正期望值
|
||||
- `types/repology.rs` — `serde_yaml::from_str("")` 返回 EOF 错误,改用 `"default"` 测试
|
||||
- `types/time.rs` — `deserialize_option_duration_ms` 的 visitor 不支持 `visit_some`,改用标准 `Option<u64>`/`Option<String>` 测试
|
||||
|
||||
### 3. 系统测试 (tests/)
|
||||
|
||||
新增/完善 3 个 Rust 集成测试文件:
|
||||
|
||||
| 文件 | 测试数 | 覆盖内容 |
|
||||
|------|--------|----------|
|
||||
| `tests/types_config_integration.rs` | 8 | Architecture FromStr, BuilderConfig untagged, CacheDirectory round-trip, RepologyEndpoint, CompressionMethod, CustomCommand YAML |
|
||||
| `tests/bake_pipeline_integration.rs` | 5 | parse_script 全 pipeline, @after, 混合 decorator, remaining_code, multiline function |
|
||||
| `tests/prebake_config_integration.rs` | 6 | PrebakeConfig minimal, builder type, envvars, bootstrap/workspace, validate pass/fail |
|
||||
|
||||
**设计决策**:
|
||||
- `bake_pipeline_integration.rs` 中未测试 `sort_function`,因为 `schedule` 模块是 private (`mod schedule;`),无法从 crate 外部访问
|
||||
- `types_config_integration.rs` 中 `CompressionMethod` 使用 `matches!()` 替代 `assert_eq!`,因该 enum 未实现 `PartialEq`(pre-existing)
|
||||
|
||||
### 4. 文档 (docs/)
|
||||
|
||||
**Vol2 管理员手册** (`docs/src/zh-CN/vol2_admin/`):
|
||||
- `environment_config.md` — 环境变量(HBW_*)、Socket 配置、工作空间、资源限制、构建状态文件
|
||||
- `deployment.md` — 安装、CLI vs Daemon 模式、预烘焙环境(Docker/Firecracker/Baremetal)、Systemd 示例、权限模型
|
||||
- `troubleshooting.md` — 日志调试、退出码对照表、常见问题(cgroup/_socket/decorator/hook/包管理器)
|
||||
|
||||
**Vol3 开发者手册** (`docs/src/zh-CN/vol3_dev/`):
|
||||
- `index.md` — 项目简介、9 crates 结构、阅读指南
|
||||
- `architecture.md` — 整体架构、数据流(Prebake→Bake→Finalize)、核心模块、类型系统、插件架构
|
||||
- `extension.md` — 添加包管理器、自定义插件(Shell/manifest)、自定义 Decorator、模板变量、邮件模板
|
||||
- `contributing.md` — 开发环境(Rust 1.85+)、代码规范、错误处理(thiserror+anyhow)、测试、提交规范、PR 流程
|
||||
|
||||
`docs/src/SUMMARY.md` 已更新导航。
|
||||
|
||||
## 已知问题(未修复实现代码)
|
||||
|
||||
以下问题属于 pre-existing,已按约束要求记录但不修复实现:
|
||||
|
||||
| 位置 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| `src/finalize/plugin/fetch/git.rs:169,176` | `test_parse_git_url_complex_url` / `test_parse_git_url_single_with_at_in_path` 失败 | `parse_git_url` 对 `@` 在 path 中的处理与测试预期不符 |
|
||||
| `src/types/compression.rs:8` | `CompressionMethod` 缺少 `PartialEq` | 集成测试需用 `matches!()` 替代 `assert_eq!()` |
|
||||
| `src/finalize/plugin/fetch/extract.rs` | `CompressionType` 缺少 `PartialEq` | 已有测试中使用 match 断言 |
|
||||
| `src/finalize/plugin/fetch/checksum.rs` | `ChecksumType` 缺少 `PartialEq` | 同上 |
|
||||
| `src/finalize/plugin/fetch/git.rs` | `GitVersion` 缺少 `Debug` | 测试中使用 match 断言 |
|
||||
|
||||
## 追加调整(用户反馈后)
|
||||
|
||||
### 5. 移除 archived/ 相关描述
|
||||
|
||||
从文档中移除了所有 `archived/` 目录相关 crate 的活跃描述:
|
||||
- `docs/src/zh-CN/vol3_dev/index.md` — 将 9 crates 列表改为仅列出 4 个活跃 crate,其余标注为已归档
|
||||
- `docs/src/zh-CN/vol3_dev/architecture.md` — 将整体架构描述改为仅列出活跃 crate,标注其余已归档
|
||||
|
||||
### 6. 移除 cgroups 相关描述
|
||||
|
||||
从以下文档中移除了 cgroups 相关内容:
|
||||
- `docs/src/zh-CN/vol2_admin/environment_config.md` — 移除 cgroup v2 资源限制章节
|
||||
- `docs/src/zh-CN/vol2_admin/troubleshooting.md` — 移除 cgroup 常见问题条目
|
||||
- `docs/src/zh-CN/vol3_dev/architecture.md` — 从数据流与核心模块描述中移除 cgroup 引用
|
||||
|
||||
### 8. 修正 HBW_USERNAME 描述
|
||||
|
||||
`docs/src/zh-CN/vol2_admin/environment_config.md` → `environment_variables.md`:
|
||||
- 将 `HBW_USERNAME` 从"构建用户名 / 默认为 vulcan / 用于权限降级"修正为"提交者用户名(来自 base / server)"
|
||||
- 明确说明与构建系统内的 Unix/Windows 用户名无关,仅用于日志、通知与审计追踪
|
||||
|
||||
### 9. 文档拆分
|
||||
|
||||
将包含多个不相关章节的文档拆分为独立文件:
|
||||
|
||||
**vol2_admin/**:
|
||||
- `environment_config.md`(环境变量 + Socket + 工作空间 + 构建状态)拆分为:
|
||||
- `environment_variables.md` — 仅环境变量
|
||||
- `socket_config.md` — 仅 Socket 配置
|
||||
- `workspace.md` — 仅工作空间
|
||||
- (构建状态内容已独立为 `status_file_format.md`,不再重复)
|
||||
|
||||
**vol3_dev/**:
|
||||
- `extension.md`(包管理器 + 插件 + Decorator + 模板变量 + 邮件模板)拆分为:
|
||||
- `add_package_manager.md`
|
||||
- `custom_plugin.md`
|
||||
- `custom_decorator.md`
|
||||
- `template_variables.md`
|
||||
- `mail_template.md`
|
||||
- `contributing.md`(开发环境 + 代码规范 + 错误处理 + 测试 + 提交规范 + PR 流程)拆分为:
|
||||
- `development_environment.md`
|
||||
- `code_style.md`
|
||||
- `commit_convention.md`
|
||||
- `pr_process.md`
|
||||
- (错误处理与测试内容已独立为 `error_handling_patterns.md` 和 `testing_strategy.md`,不再重复)
|
||||
|
||||
`docs/src/SUMMARY.md` 已同步更新导航。
|
||||
|
||||
### 7. 细粒度文档(参考 pipeline_implicit_parameter.md 风格)
|
||||
|
||||
新增 10 篇短文档(12–60 行,聚焦单一功能点,表格为主):
|
||||
|
||||
**Vol2 管理员手册**:
|
||||
- `decorator_cheatsheet.md` — 12 个 `@decorator` 语法、作用、示例、语法规则、常见组合
|
||||
- `prebake_config_reference.md` — `prebake.yml` 顶层字段、environment/resources/network/bootstrap/dependencies/envvars/cache 完整参考与示例
|
||||
- `finalize_plugin_reference.md` — 插件类型对比、mail/webhook 内置插件字段、模板变量、最小配置示例
|
||||
- `exit_code_guide.md` — 9 个退出码对照表、排查命令
|
||||
- `status_file_format.md` — `.hbwstatus` JSON Lines 格式、字段说明、读取示例
|
||||
|
||||
**Vol3 开发者手册**:
|
||||
- `error_handling_patterns.md` — thiserror/anyhow 选取原则、示例、退出码映射
|
||||
- `type_system_guide.md` — types/ 设计原则、核心类型速查、serde 定制示例、使用约束
|
||||
- `testing_strategy.md` — 测试层级对比、单元/异步/集成测试规范、运行命令
|
||||
- `plugin_system_guide.md` — Plugin trait、内部/外部插件对比、mail/webhook 配置、模板上下文
|
||||
- `engine_module_guide.md` — engine/ 模块划分、PackageManager trait、cgroup 限制、执行流程
|
||||
|
||||
`docs/src/SUMMARY.md` 已同步更新导航。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `cargo build -j 6` — ✅ 通过(30 warnings,均为 pre-existing)
|
||||
- `cargo test --lib` — 317 passed, 2 failed(pre-existing git), 3 ignored
|
||||
- `cargo test --test types_config_integration` — 8 passed
|
||||
- `cargo test --test bake_pipeline_integration` — 5 passed
|
||||
- `cargo test --test prebake_config_integration` — 6 passed
|
||||
- `mdbook build docs/` — ✅ 通过
|
||||
|
||||
## 约束遵守情况
|
||||
|
||||
- ✅ 未修改任何实现代码(函数体、结构体)
|
||||
- ✅ 未添加行内/单行注释(所有新增注释均为 `///` docstring 或已移除)
|
||||
- ✅ 未写 README.md
|
||||
- ✅ 未运行 cargo fix / cargo clippy --fix
|
||||
- ✅ 未修改 .toml / .yaml 配置文件
|
||||
- ✅ 跳过含 `todo!()` / `unimplemented!()` / `// TODO:` 的模块
|
||||
- ✅ 仅新增测试、docstring、独立文档
|
||||
- ✅ 编译并行任务数 `-j 6`
|
||||
@@ -45,6 +45,7 @@ HoneyBiscuitWorkshop is a Rust-based CI/CD system ("蜜饼工坊") with LLM-powe
|
||||
|
||||
### Rust
|
||||
- **Edition**: 2024 (non-standard, requires Rust 1.85+)
|
||||
- Deprecate `mod.rs`
|
||||
- **Naming**: `snake_case` files/modules, `PascalCase` types, `SCREAMING_SNAKE_CASE` consts
|
||||
- **Error handling**: `thiserror` + `anyhow` combo
|
||||
- **Async**: `#[tokio::main]` + `tokio` with "full" features
|
||||
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs
|
||||
index 75bce82..ad56c66 100644
|
||||
--- a/workshop-baker/src/notify.rs
|
||||
+++ b/workshop-baker/src/notify.rs
|
||||
@@ -3,15 +3,15 @@ use crate::finalize::FinalizeConfig;
|
||||
use crate::finalize::parse;
|
||||
use crate::finalize::plugin;
|
||||
use crate::finalize::plugin::fetch;
|
||||
-use crate::notify::types::BundledNotifyEvent;
|
||||
+use crate::notify::types::BundledNotificationEvent;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
|
||||
use crate::notify::types::NotificationRulesMap;
|
||||
-use crate::notify::types::NotifyEvent;
|
||||
-use crate::notify::types::NotifyEventReceiver;
|
||||
-use crate::notify::types::NotifyEventSender;
|
||||
-use crate::notify::types::NotifyPluginMap;
|
||||
+use crate::notify::types::NotificationEvent;
|
||||
+use crate::notify::types::NotificationEventReceiver;
|
||||
+use crate::notify::types::NotificationEventSender;
|
||||
+use crate::notify::types::NotificationPluginMap;
|
||||
use crate::{EventSender, ExecutionContext};
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
@@ -46,8 +46,8 @@ pub async fn notify(
|
||||
finalize_path: &Path,
|
||||
ctx: &mut ExecutionContext,
|
||||
event_tx: EventSender,
|
||||
- notifyevent_rx: &mut NotifyEventReceiver,
|
||||
- notifyevent_tx: NotifyEventSender,
|
||||
+ notifyevent_rx: &mut NotificationEventReceiver,
|
||||
+ notifyevent_tx: NotificationEventSender,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut finalize = parse(finalize_path)?;
|
||||
validate(&finalize)?;
|
||||
@@ -114,10 +114,11 @@ pub async fn notify(
|
||||
}
|
||||
|
||||
fn notification_handler(
|
||||
- plugins: &NotifyPluginMap,
|
||||
+ plugins: &NotificationPluginMap,
|
||||
config: &NotificationRulesMap,
|
||||
- event: &BundledNotifyEvent,
|
||||
+ event: &BundledNotificationEvent,
|
||||
) -> Result<(), NotifyError> {
|
||||
+
|
||||
todo!();
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
||||
}
|
||||
|
||||
// calculate p^(m-l)*t as notification delay
|
||||
-fn notify_delay(event: &NotifyEvent) -> std::time::Duration {
|
||||
+fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
|
||||
let exponent = (event.max_lives - event.lives) as i32;
|
||||
if exponent < 0 {
|
||||
log::warn!(
|
||||
diff --git a/workshop-baker/src/notify/types.rs b/workshop-baker/src/notify/types.rs
|
||||
index b852dfb..d6ccfb7 100644
|
||||
--- a/workshop-baker/src/notify/types.rs
|
||||
+++ b/workshop-baker/src/notify/types.rs
|
||||
@@ -39,13 +39,13 @@ pub struct NotificationRule{
|
||||
pub type Priority = u32;
|
||||
pub type Method = String;
|
||||
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
|
||||
-pub type BundledNotifyEvent = Vec<NotifyEvent>;
|
||||
-pub type NotifyEventReceiver = tokio::sync::mpsc::Receiver<BundledNotifyEvent>;
|
||||
-pub type NotifyEventSender = tokio::sync::mpsc::Sender<NotifyEvent>;
|
||||
-pub type NotifyPluginMap = HashMap<String, FinalizePlugin>;
|
||||
+pub type BundledNotificationEvent = Vec<NotificationEvent>;
|
||||
+pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
|
||||
+pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
|
||||
+pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
-pub struct NotifyEvent {
|
||||
+pub struct NotificationEvent {
|
||||
pub id: Uuid,
|
||||
pub stage: StagePhase,
|
||||
pub substage: String,
|
||||
@@ -58,21 +58,21 @@ pub struct NotifyEvent {
|
||||
pub target: HashSet<String>, // Target audience
|
||||
}
|
||||
|
||||
-impl Hash for NotifyEvent {
|
||||
+impl Hash for NotificationEvent {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
-impl PartialEq for NotifyEvent {
|
||||
+impl PartialEq for NotificationEvent {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
-impl Eq for NotifyEvent {}
|
||||
+impl Eq for NotificationEvent {}
|
||||
|
||||
-impl PartialOrd for NotifyEvent {
|
||||
+impl PartialOrd for NotificationEvent {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(
|
||||
self.effective_priority
|
||||
@@ -83,7 +83,7 @@ impl PartialOrd for NotifyEvent {
|
||||
}
|
||||
}
|
||||
|
||||
-impl Ord for NotifyEvent {
|
||||
+impl Ord for NotificationEvent {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
Generated
+32
@@ -2155,6 +2155,19 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.12"
|
||||
@@ -5254,6 +5267,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"gix",
|
||||
"glob",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
"libc",
|
||||
@@ -5276,9 +5290,27 @@ dependencies = [
|
||||
"topological-sort",
|
||||
"uuid",
|
||||
"which",
|
||||
"workshop-engine",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "workshop-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"duration-str",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
|
||||
@@ -20,6 +20,7 @@ name = "engine_integration"
|
||||
path = "tests/engine_integration.rs"
|
||||
|
||||
[dependencies]
|
||||
workshop-engine = { path = "../workshop-engine" }
|
||||
anyhow = "1.0.100"
|
||||
bollard = { version = "0.20.0", features = ["buildkit", "chrono", "ssh"] }
|
||||
cgroups-rs = "0.5.0"
|
||||
@@ -56,6 +57,7 @@ gix = "0.81.0"
|
||||
sha2 = "0.10"
|
||||
flate2 = "1.0"
|
||||
zstd = "0.13"
|
||||
globset = "0.4.18"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use crate::ExecutionContext;
|
||||
use workshop_engine::{Engine, EventSender, ExecutionContext};
|
||||
use crate::bake::parser::Function;
|
||||
use crate::bake::constant::{DEFAULT_WORKSPACE, TRIVIAL_SCRIPT_NAME};
|
||||
use crate::bake::error::BakeError;
|
||||
use crate::cli::Cli;
|
||||
use crate::constant::{DEFAULT_WORKSPACE, STANDALONE_STATUS_DIR, TRIVIAL_SCRIPT_NAME};
|
||||
use crate::engine::EventSender;
|
||||
use crate::error::BakeError;
|
||||
use crate::prebake::PrebakeConfig;
|
||||
use crate::prebake::security::get_drop_after;
|
||||
use crate::prebake::stage::PrebakeStage;
|
||||
use crate::types::buildstatus::{StageInfo, StagePhase, StageResult, write_stage_status};
|
||||
use crate::{Engine, prebake};
|
||||
use chrono::Utc;
|
||||
use crate::prebake;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod builder;
|
||||
pub mod constant;
|
||||
mod decorator;
|
||||
pub mod parser;
|
||||
pub mod error;
|
||||
mod schedule;
|
||||
|
||||
pub async fn bake(
|
||||
@@ -51,7 +50,6 @@ pub async fn bake(
|
||||
}
|
||||
|
||||
if !has_pipeline || trivial {
|
||||
let stage_start = Utc::now();
|
||||
let function = Function {
|
||||
name: TRIVIAL_SCRIPT_NAME.to_string(),
|
||||
decorators: vec![],
|
||||
@@ -60,32 +58,11 @@ pub async fn bake(
|
||||
let script =
|
||||
builder::build_script(&function, bake_base_path, &workspace, None, use_template)?;
|
||||
let result = engine.execute_script(&script, &ctx, &event_tx).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "trivial".to_string(),
|
||||
phase: StagePhase::Bake,
|
||||
substage: "trivial".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
} else {
|
||||
let sorted_functions = schedule::sort_function(functions.pipeline_functions)?;
|
||||
let remaining = functions.remaining_code;
|
||||
for func in sorted_functions {
|
||||
let stage_start = Utc::now();
|
||||
let mut modified_func = func.clone();
|
||||
modified_func.body = remaining.clone() + &func.body;
|
||||
let script = builder::build_script(
|
||||
@@ -96,26 +73,6 @@ pub async fn bake(
|
||||
use_template,
|
||||
)?;
|
||||
let result = engine.execute_script(&script, &ctx, &event_tx).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: func.name.clone(),
|
||||
phase: StagePhase::Bake,
|
||||
substage: func.name.clone(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::bake::decorator::Decorator;
|
||||
use crate::bake::parser::Function;
|
||||
use crate::constant::TRIVIAL_SCRIPT_NAME;
|
||||
use crate::bake::constant::{PIPELINE_SKIP_ERRORCODE, TRIVIAL_SCRIPT_NAME};
|
||||
use crate::bake::error::BakeError;
|
||||
use minijinja::{Environment, context};
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -8,9 +9,6 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::constant::PIPELINE_SKIP_ERRORCODE;
|
||||
use crate::error::BakeError;
|
||||
|
||||
/// Template for use_template=false, which just executes the main body without any wrapping.
|
||||
const TRIVIAL_TEMPLATE: &str = "{{ main }}";
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub const DEFAULT_WORKSPACE: &str = "/workspace";
|
||||
pub const BAKE_SCRIPT_PATH: &str = "bake.sh";
|
||||
pub const TRIVIAL_SCRIPT_NAME: &str = "BAKE_TRIVIAL";
|
||||
pub const PIPELINE_SKIP_ERRORCODE: u8 = 233;
|
||||
@@ -3,7 +3,7 @@ use regex::Regex;
|
||||
use std::fmt::Display;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::error::BakeError;
|
||||
use crate::bake::error::BakeError;
|
||||
|
||||
lazy_static! {
|
||||
// "# @decorator" or "# @decorator(parameters)"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use workshop_engine::{
|
||||
ExecutionError,
|
||||
error::{
|
||||
EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
||||
HasExitCode,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BakeError {
|
||||
#[error("Failed to generate staged script {script}: {reason}")]
|
||||
ScriptGenerationFailed { script: String, reason: String },
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
TemplateError(#[from] minijinja::Error),
|
||||
|
||||
#[error("Circular Dependency detected in bake.sh")]
|
||||
CircularDependency(Vec<String>),
|
||||
|
||||
#[error("Unknown @after of function {0}: {1}")]
|
||||
UnknownDependency(String, String),
|
||||
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Failed to parse decorator @{decorator} (line #{line_num}): {reason}")]
|
||||
DecoratorParseError {
|
||||
decorator: String,
|
||||
line_num: usize,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Script execution error: {0}")]
|
||||
ScriptExecutionError(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
impl HasExitCode for BakeError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
BakeError::ScriptGenerationFailed { .. } => EXITCODE_IO_ERROR,
|
||||
BakeError::TemplateError(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::CircularDependency(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::UnknownDependency(..) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
BakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::DecoratorParseError { .. } => EXITCODE_PARSE_ERROR,
|
||||
BakeError::ScriptExecutionError(e) => e.exit_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::decorator::{Decorator, parse_decorator};
|
||||
use crate::bake::BakeError;
|
||||
use crate::bake::error::BakeError;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use shlex::Shlex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{decorator::Decorator, parser::Function};
|
||||
use crate::error::BakeError;
|
||||
use crate::bake::error::BakeError;
|
||||
use std::collections::HashMap;
|
||||
use topological_sort::TopologicalSort;
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
pub const DEFAULT_WORKSPACE: &str = "/workspace";
|
||||
pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh";
|
||||
pub const BAKE_SCRIPT_PATH: &str = "bake.sh";
|
||||
pub const PIPELINE_SKIP_ERRORCODE: u8 = 233;
|
||||
pub const TRIVIAL_SCRIPT_NAME: &str = "BAKE_TRIVIAL";
|
||||
pub const FINALIZE_PLUGIN_MANIFEST: &str = "manifest";
|
||||
pub const FINALIZE_PLUGIN_MANIFEST_EXTENSION: &[&str] = &[".yml", ".yaml"];
|
||||
pub const FINALIZE_SHELL_ENVPREFIX: &str = "HBW_ARG";
|
||||
pub const FINALIZE_STANDALONE_PLUGIN_PATH: &str = "/tmp/baker/plugin";
|
||||
|
||||
/// Standalone mode build status file directory
|
||||
pub const STANDALONE_STATUS_DIR: &str = "/tmp";
|
||||
/// Standalone mode build status file name
|
||||
pub const STANDALONE_STATUS_FILE: &str = ".hbwstatus";
|
||||
@@ -1,17 +0,0 @@
|
||||
//! Engine module - execution engine for workshop-baker
|
||||
//!
|
||||
//! This module provides the core execution functionality for running commands
|
||||
//! in isolated environments with resource management.
|
||||
|
||||
pub mod cgroups;
|
||||
pub mod executor;
|
||||
pub mod pm;
|
||||
pub mod repology;
|
||||
pub mod types;
|
||||
pub mod upm;
|
||||
|
||||
// Re-export commonly used types for convenience
|
||||
pub use types::{
|
||||
Engine, EventSender, ExecutionContext, ExecutionError, ExecutionEvent, ExecutionResult,
|
||||
ResourceLimits, ResourceUsage, StreamType,
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
# workshop-baker/src/engine
|
||||
|
||||
**Generated:** 2026-04-22
|
||||
**Commit:** 7b3b71a
|
||||
|
||||
Execution runtime with resource isolation and package management.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
engine/
|
||||
├── executor.rs # Script execution with cgroups, pipes, exit codes
|
||||
├── cgroups.rs # Linux cgroup resource limits (memory, CPU, IO)
|
||||
├── types.rs # ResourceLimits, ExecutionResult, ProcessConfig
|
||||
├── pm.rs # Package manager detection (apt, pacman, etc.)
|
||||
├── pm/ # PM implementations
|
||||
│ ├── apt.rs # Debian/Ubuntu (WIP - todo!() stubs)
|
||||
│ └── pacman.rs # Arch Linux
|
||||
├── upm.rs # User package managers (Nix/Guix)
|
||||
├── upm/custom.rs # Custom PM support (unimplemented)
|
||||
└── repology.rs # Repology API for package name resolution
|
||||
└── local.rs # Python-based local repology (non-standard)
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Execute script with limits | `executor.rs:Executor::execute_script()` |
|
||||
| Resource limits | `types.rs:56` - `ResourceLimits` struct |
|
||||
| Cgroup management | `cgroups.rs` - CgroupManager v1/v2 |
|
||||
| Package detection | `pm.rs:detection()` |
|
||||
| Repology lookup | `repology.rs` - RepologyEndpoint |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Executor**: Lightweight wrapper around CgroupManager for isolation
|
||||
- **ResourceLimits**: Memory, CPU, IO bounds passed to cgroups
|
||||
- **Package Manager Detection**: Auto-detects from `/etc/os-release`
|
||||
- **WIP**: apt.rs has `todo!()` stubs (lines 22, 45, 50)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
1. **Python in Rust** — `repology/local.rs` embeds Python repology client
|
||||
2. **Unsafe libc** — Multiple `unsafe { libc::geteuid() }` in security.rs (parent)
|
||||
3. **Unwrap heavy** — cgroups.rs has many `.unwrap()` calls
|
||||
@@ -1,46 +0,0 @@
|
||||
pub mod local;
|
||||
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RepologyError {
|
||||
Unimplemented,
|
||||
RemoteNotSupported,
|
||||
}
|
||||
|
||||
pub async fn resolve_package_names(
|
||||
packages: &[String],
|
||||
mapped: bool,
|
||||
endpoint: &RepologyEndpoint,
|
||||
target_pm: &str,
|
||||
) -> Result<HashMap<String, Vec<String>>, RepologyError> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
if packages.is_empty() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
if mapped {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
match endpoint {
|
||||
RepologyEndpoint::Local | RepologyEndpoint::Default => {
|
||||
let resolved = local::resolve(packages, target_pm).await;
|
||||
if !resolved.is_empty() {
|
||||
result.insert(target_pm.to_string(), resolved);
|
||||
}
|
||||
}
|
||||
RepologyEndpoint::None => {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
}
|
||||
RepologyEndpoint::Disabled => {}
|
||||
RepologyEndpoint::Remote | RepologyEndpoint::Server => {
|
||||
return Err(RepologyError::Unimplemented);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use os_info::Info;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
// Obsolete?
|
||||
pub trait HasExitCode: Display + Debug + Send + Sync + Sized + 'static {
|
||||
fn exit_code(&self) -> i32;
|
||||
}
|
||||
|
||||
pub const EXITCODE_OK: i32 = 0;
|
||||
pub const EXITCODE_GENERAL_ERROR: i32 = 1;
|
||||
pub const EXITCODE_INVALID_ARGS: i32 = 2;
|
||||
pub const EXITCODE_IO_ERROR: i32 = 3;
|
||||
pub const EXITCODE_PARSE_ERROR: i32 = 4;
|
||||
pub const EXITCODE_EXECUTION_ERROR: i32 = 5;
|
||||
pub const EXITCODE_TIMEOUT: i32 = 124;
|
||||
pub const EXITCODE_PRIV_DROP_FAILED: i32 = 201;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BakeError {
|
||||
#[error("Failed to generate staged script {script}: {reason}")]
|
||||
ScriptGenerationFailed { script: String, reason: String },
|
||||
|
||||
#[error("Template error: {0}")]
|
||||
TemplateError(#[from] minijinja::Error),
|
||||
|
||||
#[error("Circular Dependency detected in bake.sh")]
|
||||
CircularDependency(Vec<String>),
|
||||
|
||||
#[error("Unknown @after of function {0}: {1}")]
|
||||
UnknownDependency(String, String),
|
||||
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Failed to parse decorator @{decorator} (line #{line_num}): {reason}")]
|
||||
DecoratorParseError {
|
||||
decorator: String,
|
||||
line_num: usize,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Script execution error: {0}")]
|
||||
ScriptExecutionError(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PrebakeError {
|
||||
#[error("Privilege drop failed: {0}")]
|
||||
PrivDropFailed(String),
|
||||
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
|
||||
#[error("Invalid privilege state: current_uid={current_uid}, target_uid={target_uid}")]
|
||||
InvalidPrivilegeState { current_uid: u32, target_uid: u32 },
|
||||
|
||||
#[error("Invalid stage: current={current}, expected={expected}")]
|
||||
InvalidStage { current: String, expected: String },
|
||||
|
||||
#[error("Failed to validate prebake.yml.")]
|
||||
ValidateError(),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Bad prebake.yml: {0}")]
|
||||
ConfigError(String),
|
||||
}
|
||||
|
||||
impl HasExitCode for PrebakeError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::InvalidPrivilegeState { .. } => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::ValidateError() => EXITCODE_PARSE_ERROR,
|
||||
PrebakeError::InvalidStage { .. } => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
PrebakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||
PrebakeError::ConfigError(_) => EXITCODE_PARSE_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BootstrapError {
|
||||
#[error("Failed to write bootstrap script: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to find bootstrap.sh: {0}")]
|
||||
ScriptNotFound(PathBuf),
|
||||
|
||||
#[error("Failed to execute bootstrap.sh: {0}")]
|
||||
ExecutionError(#[from] ExecutionError),
|
||||
|
||||
#[error("Failed to create user {0}: {1}")]
|
||||
UserCreationFailed(String, String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DependencyError {
|
||||
#[error("No supported package manager found for {0}")]
|
||||
UnsupportedPlatform(Info),
|
||||
|
||||
#[error("Unknown package manager: {0}")]
|
||||
UnknownManager(String),
|
||||
|
||||
#[error("Failed to parse config")]
|
||||
ParseError(),
|
||||
|
||||
#[error("Stage {stage} failed: {source}")]
|
||||
StageFailed {
|
||||
stage: &'static str,
|
||||
#[source]
|
||||
source: ExecutionError,
|
||||
},
|
||||
|
||||
#[error("Custom command execution failed: {0}")]
|
||||
CustomFailed(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExecutionError {
|
||||
#[error("Invalid command: {0}")]
|
||||
InvalidCommand(String),
|
||||
|
||||
#[error("Execution failed: {0}")]
|
||||
ExecutionFailed(String),
|
||||
|
||||
#[error("Permission denied: uid={euid}, gid={egid}: {reason}")]
|
||||
PermissionDenied {
|
||||
euid: u32,
|
||||
egid: u32,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("Script not found: {0}")]
|
||||
ScriptNotFound(PathBuf),
|
||||
|
||||
#[error("Package manager not found")]
|
||||
PackageManagerNotFound,
|
||||
}
|
||||
|
||||
impl HasExitCode for BakeError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
BakeError::ScriptGenerationFailed { .. } => EXITCODE_IO_ERROR,
|
||||
BakeError::TemplateError(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::CircularDependency(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::UnknownDependency(..) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
BakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||
BakeError::DecoratorParseError { .. } => EXITCODE_PARSE_ERROR,
|
||||
BakeError::ScriptExecutionError(e) => e.exit_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasExitCode for ExecutionError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
|
||||
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
|
||||
ExecutionError::PermissionDenied { .. } => EXITCODE_GENERAL_ERROR,
|
||||
ExecutionError::Timeout => EXITCODE_TIMEOUT,
|
||||
ExecutionError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
ExecutionError::ScriptNotFound(_) => EXITCODE_INVALID_ARGS,
|
||||
ExecutionError::PackageManagerNotFound => EXITCODE_GENERAL_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
pub mod config;
|
||||
pub mod constant;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod plugin;
|
||||
pub mod stage;
|
||||
pub mod template;
|
||||
pub mod types;
|
||||
|
||||
use crate::cli::Cli;
|
||||
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||
use crate::constant::STANDALONE_STATUS_DIR;
|
||||
use crate::engine::EventSender;
|
||||
use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||
use workshop_engine::EventSender;
|
||||
use crate::finalize::plugin::fetch::{self, FetchArgument};
|
||||
use crate::types::buildstatus::{BuildStatus, StageResult, read_build_status};
|
||||
use crate::types::buildstatus::{StageInfo, StagePhase};
|
||||
use chrono::Utc;
|
||||
use std::path::PathBuf;
|
||||
// use crate::finalize::error::PluginError;
|
||||
pub use config::*;
|
||||
pub use error::FinalizeError;
|
||||
use std::path::Path;
|
||||
@@ -28,7 +25,7 @@ pub fn parse(path: &Path) -> Result<FinalizeConfig, FinalizeError> {
|
||||
pub async fn finalize(
|
||||
finalize_path: &Path,
|
||||
cli: &Cli,
|
||||
ctx: &mut crate::ExecutionContext,
|
||||
ctx: &mut workshop_engine::ExecutionContext,
|
||||
event_tx: EventSender,
|
||||
) -> Result<(), FinalizeError> {
|
||||
let mut finalize = parse(finalize_path)?;
|
||||
@@ -76,13 +73,9 @@ pub async fn finalize(
|
||||
)
|
||||
.await;
|
||||
|
||||
// Read build status from prior stages
|
||||
let build_status = read_build_status(STANDALONE_STATUS_DIR).unwrap_or_default();
|
||||
let final_result = build_status.final_result();
|
||||
|
||||
// Determine if we should continue with artifacts/latehook
|
||||
// If prior stages failed, we still send notification but skip artifacts
|
||||
let prior_failed = build_status.has_failures() || earlyhook_result.is_err();
|
||||
let prior_failed = earlyhook_result.is_err();
|
||||
|
||||
// Prepare artifacts
|
||||
// TODO: artifact stage implementation
|
||||
@@ -96,20 +89,5 @@ pub async fn finalize(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Write finalize success status
|
||||
let _ = crate::types::buildstatus::write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "finalize".to_string(),
|
||||
phase: StagePhase::Finalize,
|
||||
substage: "clean".to_string(),
|
||||
result: StageResult::Success,
|
||||
duration_ms: 0,
|
||||
started_at: Utc::now(),
|
||||
finished_at: Utc::now(),
|
||||
error_message: None,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
//! This module defines the configuration schema for the finalize stage,
|
||||
//! which handles artifact collection, publishing, and notifications.
|
||||
|
||||
use crate::types::command::CustomCommand;
|
||||
use crate::types::compression::CompressionMethod;
|
||||
use crate::types::time::deserialize_duration_ms;
|
||||
use workshop_engine::{CustomCommand, deserialize_duration_ms};
|
||||
use crate::finalize::types::compression::CompressionMethod;
|
||||
use semver::{Version, VersionReq};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value;
|
||||
@@ -180,7 +179,7 @@ pub struct NotificationTemplateDef {
|
||||
}
|
||||
|
||||
/// Notification trigger type.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Trigger {
|
||||
OnSuccess,
|
||||
OnFailure,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
pub const FINALIZE_PLUGIN_MANIFEST: &str = "manifest";
|
||||
pub const FINALIZE_PLUGIN_MANIFEST_EXTENSION: &[&str] = &[".yml", ".yaml"];
|
||||
pub const FINALIZE_SHELL_ENVPREFIX: &str = "HBW_ARG";
|
||||
|
||||
/// Standalone mode: directory where fetched plugins are stored
|
||||
pub const FINALIZE_STANDALONE_PLUGIN_PATH: &str = "/tmp/baker/plugin";
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
use workshop_engine::{
|
||||
ExecutionError,
|
||||
error::{
|
||||
EXITCODE_EXECUTION_ERROR, EXITCODE_GENERAL_ERROR, EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::engine::{ExecutionEvent, StreamType};
|
||||
use workshop_engine::{ExecutionEvent, StreamType};
|
||||
|
||||
/// Receives and processes execution events for the finalize stage.
|
||||
/// Logs task lifecycle events (started, completed, failed) and streaming output (stdout/stderr)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION};
|
||||
use crate::finalize::constant::{FINALIZE_PLUGIN_MANIFEST, FINALIZE_PLUGIN_MANIFEST_EXTENSION};
|
||||
use crate::finalize::RawPluginMap;
|
||||
use crate::finalize::plugin::metadata::{PluginMetadata, PluginType};
|
||||
use crate::{EventSender, ExecutionContext, finalize::error::PluginError};
|
||||
@@ -16,7 +16,7 @@ mod shell;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FinalizePlugin {
|
||||
metadata: PluginMetadata,
|
||||
pub metadata: PluginMetadata,
|
||||
argument: serde_yaml::Value,
|
||||
pub entry: Arc<dyn AsyncPluginFn>,
|
||||
}
|
||||
|
||||
@@ -86,12 +86,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_detect_checksum_type_sha256() {
|
||||
assert_eq!(detect_checksum_type("a".repeat(64).as_str()).unwrap(), Some(ChecksumType::Sha256));
|
||||
assert_eq!(
|
||||
detect_checksum_type("a".repeat(64).as_str()).unwrap(),
|
||||
Some(ChecksumType::Sha256)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_checksum_type_sha512() {
|
||||
assert_eq!(detect_checksum_type("a".repeat(128).as_str()).unwrap(), Some(ChecksumType::Sha512));
|
||||
assert_eq!(
|
||||
detect_checksum_type("a".repeat(128).as_str()).unwrap(),
|
||||
Some(ChecksumType::Sha512)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -160,14 +160,20 @@ mod tests {
|
||||
fn test_parse_git_url_mixed_version() {
|
||||
let (url, version) = parse_git_url("https://github.com/user/repo@main:abc123");
|
||||
assert_eq!(url, "https://github.com/user/repo");
|
||||
assert_eq!(version, GitVersion::Mixed("main".to_string(), "abc123".to_string()));
|
||||
assert_eq!(
|
||||
version,
|
||||
GitVersion::Mixed("main".to_string(), "abc123".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_git_url_complex_url() {
|
||||
let (url, version) = parse_git_url("git@github.com:user/repo.git@tag:commit");
|
||||
assert_eq!(url, "git@github.com:user/repo.git");
|
||||
assert_eq!(version, GitVersion::Mixed("tag".to_string(), "commit".to_string()));
|
||||
assert_eq!(
|
||||
version,
|
||||
GitVersion::Mixed("tag".to_string(), "commit".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code in this module PARTIALLY OR FULLY utilized AI Coding Agent.
|
||||
use crate::types::time::deserialize_duration_ms;
|
||||
use workshop_engine::deserialize_duration_ms;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Email address representation supporting plain string or structured object.
|
||||
@@ -433,7 +433,9 @@ mod tests {
|
||||
fn test_email_address_serde_object() {
|
||||
let json = r#"{"name":"Test User","email":"test@example.com"}"#;
|
||||
let addr: EmailAddress = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(addr, EmailAddress::Object { name, email } if name == "Test User" && email == "test@example.com"));
|
||||
assert!(
|
||||
matches!(addr, EmailAddress::Object { name, email } if name == "Test User" && email == "test@example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -447,7 +449,9 @@ mod tests {
|
||||
fn test_recipients_serde_multiple() {
|
||||
let json = r#"["a@example.com","b@example.com"]"#;
|
||||
let recipients: Recipients = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(recipients, Recipients::Multiple(v) if v == vec!["a@example.com".to_string(), "b@example.com".to_string()]));
|
||||
assert!(
|
||||
matches!(recipients, Recipients::Multiple(v) if v == vec!["a@example.com".to_string(), "b@example.com".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::notify::types::NotificationRenderer;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -13,9 +14,14 @@ pub struct PluginMetadata {
|
||||
/// Plugin type classification.
|
||||
#[serde(default)]
|
||||
pub plugin_type: PluginType,
|
||||
/// (Notification) Renderer for plugin output.
|
||||
#[serde(default)]
|
||||
pub renderer: Option<NotificationRenderer>,
|
||||
|
||||
/// Additional YAML fields not captured by struct.
|
||||
#[serde(flatten)]
|
||||
pub value: serde_yaml::Value,
|
||||
|
||||
/// Filesystem path to plugin definition.
|
||||
#[serde(skip)]
|
||||
pub fspath: PathBuf,
|
||||
@@ -29,6 +35,7 @@ impl Default for PluginMetadata {
|
||||
entrypoint: String::new(),
|
||||
plugin_type: PluginType::Unknown,
|
||||
value: serde_yaml::Value::Null,
|
||||
renderer: None,
|
||||
fspath: PathBuf::new(),
|
||||
}
|
||||
}
|
||||
@@ -79,6 +86,7 @@ mod tests {
|
||||
plugin_type: PluginType::Shell,
|
||||
value: serde_yaml::Value::Null,
|
||||
fspath: PathBuf::from("/path/to/plugin"),
|
||||
renderer: None,
|
||||
};
|
||||
let cloned = meta.clone();
|
||||
assert_eq!(cloned.name, meta.name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::Engine;
|
||||
use crate::constant::FINALIZE_SHELL_ENVPREFIX;
|
||||
use crate::finalize::constant::FINALIZE_SHELL_ENVPREFIX;
|
||||
use crate::finalize::plugin::PluginError;
|
||||
use crate::finalize::plugin::PluginMetadata;
|
||||
use crate::{EventSender, ExecutionContext, finalize::plugin::AsyncPluginFn};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::types::DeltaExecutionContext;
|
||||
use crate::engine::{Engine, EventSender, ExecutionError};
|
||||
use workshop_engine::{DeltaExecutionContext, Engine, EventSender, ExecutionError, CustomCommand};
|
||||
use crate::finalize::plugin::{PluginMap, call_named_plugin};
|
||||
use crate::types::command::CustomCommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Executes post-build finalize hooks, supporting both plugin and shell command hooks.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod compression;
|
||||
@@ -2,10 +2,7 @@
|
||||
// Selective exports: only modules used by external callers or tests
|
||||
|
||||
pub mod bake;
|
||||
pub mod constant;
|
||||
pub mod daemon;
|
||||
pub mod engine;
|
||||
pub mod error;
|
||||
pub mod finalize;
|
||||
pub mod monitor;
|
||||
pub mod notify;
|
||||
@@ -16,8 +13,8 @@ pub mod types;
|
||||
// External crates re-exports
|
||||
pub use config;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use engine::{Engine, EventSender, ExecutionContext, ExecutionError, ExecutionResult};
|
||||
// Re-export commonly used types from workshop-engine
|
||||
pub use workshop_engine::{Engine, EventSender, ExecutionContext, ExecutionError, ExecutionResult};
|
||||
|
||||
// CLI types used by main (keep here since they're used in daemon)
|
||||
pub use clap::{Parser, Subcommand};
|
||||
|
||||
@@ -4,11 +4,10 @@ use workshop_baker::{
|
||||
bake::bake,
|
||||
cli::{BareCommands, Cli, Commands, Parser},
|
||||
daemon,
|
||||
engine::{EventSender, ExecutionContext},
|
||||
error::HasExitCode,
|
||||
finalize::finalize,
|
||||
prebake::prebake,
|
||||
};
|
||||
use workshop_engine::{error::HasExitCode, EventSender, ExecutionContext};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -69,11 +68,11 @@ async fn main() {
|
||||
if let Err(ref e) = result {
|
||||
let code = match command {
|
||||
BareCommands::Prebake { .. } => e
|
||||
.downcast_ref::<workshop_baker::error::PrebakeError>()
|
||||
.downcast_ref::<workshop_baker::prebake::error::PrebakeError>()
|
||||
.map(|pe| pe.exit_code())
|
||||
.unwrap_or(1),
|
||||
BareCommands::Bake { .. } => e
|
||||
.downcast_ref::<workshop_baker::error::BakeError>()
|
||||
.downcast_ref::<workshop_baker::bake::error::BakeError>()
|
||||
.map(|be| be.exit_code())
|
||||
.unwrap_or(1),
|
||||
BareCommands::Finalize { .. } => e
|
||||
|
||||
+239
-12
@@ -1,24 +1,37 @@
|
||||
use crate::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||
use crate::finalize::constant::FINALIZE_STANDALONE_PLUGIN_PATH;
|
||||
use crate::finalize::FinalizeConfig;
|
||||
use crate::finalize::NotificationConfig;
|
||||
use crate::finalize::parse;
|
||||
use crate::finalize::plugin;
|
||||
use crate::finalize::plugin::fetch;
|
||||
use crate::notify::types::BundledNotificationEvent;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_FACTOR;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_DELAY_TIME;
|
||||
use crate::notify::types::NOTIFY_TEMPORARY_MAX_TIME;
|
||||
use crate::notify::types::NotificationRulesMap;
|
||||
use crate::notify::types::Method;
|
||||
use crate::notify::types::NOTIFICATION_TEMPORARY_DELAY_FACTOR;
|
||||
use crate::notify::types::NOTIFICATION_TEMPORARY_DELAY_TIME;
|
||||
use crate::notify::types::NOTIFICATION_TEMPORARY_MAX_TIME;
|
||||
use crate::notify::types::NotificationEvent;
|
||||
use crate::notify::types::NotificationEventReceiver;
|
||||
use crate::notify::types::NotificationEventSender;
|
||||
use crate::notify::types::NotificationPluginMap;
|
||||
use crate::notify::types::NotificationRenderer;
|
||||
use crate::notify::types::NotificationRule;
|
||||
use crate::notify::types::NotificationRulesMap;
|
||||
use crate::{EventSender, ExecutionContext};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use crate::notify::types::ParsedTrigger;
|
||||
use crate::notify::types::Matchable;
|
||||
use globset::{Glob, GlobSetBuilder};
|
||||
use crate::finalize::config::TriggerItemRaw;
|
||||
use crate::finalize::NotificationTemplate;
|
||||
use crate::notify::types::NOTIFICATION_DEFAULT_FALLIBLE;
|
||||
use crate::notify::types::NOTIFICATION_DEFAULT_MAX_LIVES;
|
||||
use crate::notify::types::NOTIFICATION_DEFAULT_TIMEOUT_MS;
|
||||
use crate::notify::types::NOTIFICATION_DEFAULT_RETRY_BACKOFF;
|
||||
|
||||
mod error;
|
||||
mod types;
|
||||
pub mod types;
|
||||
use error::NotifyError;
|
||||
|
||||
fn validate(finalize: &FinalizeConfig) -> anyhow::Result<()> {
|
||||
@@ -69,6 +82,8 @@ pub async fn notify(
|
||||
}
|
||||
let notify_plugins = plugin::register(finalize.plugin, Some(&plugin_list))?;
|
||||
|
||||
let notification_rules = parse_rules(&finalize.notification);
|
||||
|
||||
// loop {
|
||||
// tokio::select! {
|
||||
// Some(events) = notifyevent_rx.recv() => {
|
||||
@@ -116,12 +131,224 @@ pub async fn notify(
|
||||
fn notification_handler(
|
||||
plugins: &NotificationPluginMap,
|
||||
config: &NotificationRulesMap,
|
||||
event: &BundledNotificationEvent,
|
||||
event: &NotificationEvent,
|
||||
) -> Result<(), NotifyError> {
|
||||
for (priority, ruleset) in config.iter() {
|
||||
for (method, rules) in ruleset.iter() {
|
||||
if !event.target.contains(method) {
|
||||
log::trace!("target does not contain method name: {}", method);
|
||||
continue;
|
||||
}
|
||||
for (rule_id, rule) in rules.iter().enumerate() {
|
||||
if !rule.enabled {
|
||||
log::trace!("rule is not enabled: {}", rule_id);
|
||||
continue;
|
||||
}
|
||||
if rule.accept(&event.outcome, &event.stage, &event.substage) {
|
||||
let plugin = plugins
|
||||
.get(method)
|
||||
.ok_or(NotifyError::UnknownPlugin(method.clone()))?;
|
||||
let renderer = plugin.metadata.renderer.clone().unwrap_or_default();
|
||||
match renderer {
|
||||
NotificationRenderer::Server => {
|
||||
// ...
|
||||
todo!();
|
||||
}
|
||||
NotificationRenderer::Plugin => {
|
||||
unimplemented!();
|
||||
return Err(NotifyError::UnsupportedRenderer(method.clone(), renderer));
|
||||
}
|
||||
NotificationRenderer::Interactive => {
|
||||
unimplemented!();
|
||||
return Err(NotifyError::UnsupportedRenderer(method.clone(), renderer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn parse_trigger_item_raw(item: &TriggerItemRaw) -> Vec<ParsedTrigger> {
|
||||
match item {
|
||||
TriggerItemRaw::Simple(s) => {
|
||||
match s.as_str() {
|
||||
"on_success" => vec![ParsedTrigger::OnSuccess],
|
||||
"on_failure" => vec![ParsedTrigger::OnFailure],
|
||||
s if s.starts_with("on_finish_of:") => {
|
||||
let pattern = s.trim_start_matches("on_finish_of:").trim();
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
builder.add(Glob::new(pattern).expect("Invalid glob pattern"));
|
||||
let glob_set = builder.build().expect("Failed to compile glob set");
|
||||
vec![ParsedTrigger::OnFinishOf(Matchable::Glob(glob_set))]
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Unknown trigger string: {}", s);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
TriggerItemRaw::Map(map) => {
|
||||
let mut triggers = Vec::new();
|
||||
for (key, patterns) in map {
|
||||
match key.as_str() {
|
||||
"on_finish_of" => {
|
||||
for pattern in patterns {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
builder.add(Glob::new(pattern).expect("Invalid glob pattern"));
|
||||
let glob_set = builder.build().expect("Failed to compile glob set");
|
||||
triggers.push(ParsedTrigger::OnFinishOf(Matchable::Glob(glob_set)));
|
||||
}
|
||||
}
|
||||
"on_success" => triggers.push(ParsedTrigger::OnSuccess),
|
||||
"on_failure" => triggers.push(ParsedTrigger::OnFailure),
|
||||
_ => {
|
||||
log::warn!("Unknown trigger key: {}", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
triggers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_templates(
|
||||
method_template: &NotificationTemplate,
|
||||
policy_template: &Option<serde_yaml::Value>,
|
||||
) -> NotificationTemplate {
|
||||
let mut merged = method_template.clone();
|
||||
if let Some(policy_tmpl) = policy_template {
|
||||
if let Ok(policy_template) =
|
||||
serde_yaml::from_value::<NotificationTemplate>(policy_tmpl.clone())
|
||||
{
|
||||
if policy_template.schema.is_some() {
|
||||
merged.schema = policy_template.schema;
|
||||
}
|
||||
if policy_template.on_success.is_some() {
|
||||
merged.on_success = policy_template.on_success;
|
||||
}
|
||||
if policy_template.on_failure.is_some() {
|
||||
merged.on_failure = policy_template.on_failure;
|
||||
}
|
||||
if policy_template.on_finish_of.is_some() {
|
||||
merged.on_finish_of = policy_template.on_finish_of;
|
||||
}
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn extract_overrides(overrides: &serde_yaml::Value) -> (bool, u32, std::time::Duration, f64) {
|
||||
let fallible = overrides
|
||||
.get("fallible")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(NOTIFICATION_DEFAULT_FALLIBLE);
|
||||
let max_lives = overrides
|
||||
.get("max_lives")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(NOTIFICATION_DEFAULT_MAX_LIVES as u64) as u32;
|
||||
let timeout_ms = overrides
|
||||
.get("timeout_ms")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(NOTIFICATION_DEFAULT_TIMEOUT_MS);
|
||||
let retry_backoff = overrides
|
||||
.get("retry_backoff")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(NOTIFICATION_DEFAULT_RETRY_BACKOFF);
|
||||
(
|
||||
fallible,
|
||||
max_lives,
|
||||
std::time::Duration::from_millis(timeout_ms),
|
||||
retry_backoff,
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_rules(config: &NotificationConfig) -> NotificationRulesMap {
|
||||
let mut rules = NotificationRulesMap::default();
|
||||
for (priority, group) in &config.groups {
|
||||
let mut ruleset: HashMap<Method, Vec<NotificationRule>> = HashMap::new();
|
||||
for (method_name, method) in group {
|
||||
let mut method_rules: Vec<NotificationRule> = Vec::new();
|
||||
|
||||
if method.policy.is_empty() {
|
||||
let trigger: Vec<ParsedTrigger> = method
|
||||
.trigger
|
||||
.iter()
|
||||
.flat_map(parse_trigger_item_raw)
|
||||
.collect();
|
||||
let template = method.template.clone();
|
||||
let (fallible, max_lives, timeout, retry_backoff) =
|
||||
extract_overrides(&method.config);
|
||||
method_rules.push(NotificationRule {
|
||||
enabled: true,
|
||||
trigger,
|
||||
template,
|
||||
fallible,
|
||||
max_lives,
|
||||
timeout,
|
||||
config: method.config.clone(),
|
||||
retry_backoff,
|
||||
});
|
||||
} else {
|
||||
for policy in &method.policy {
|
||||
let trigger: Vec<ParsedTrigger> = if policy.trigger.is_empty() {
|
||||
method
|
||||
.trigger
|
||||
.iter()
|
||||
.flat_map(parse_trigger_item_raw)
|
||||
.collect()
|
||||
} else {
|
||||
policy
|
||||
.trigger
|
||||
.iter()
|
||||
.flat_map(parse_trigger_item_raw)
|
||||
.collect()
|
||||
};
|
||||
let template = merge_templates(&method.template, &Some(policy.template.clone()));
|
||||
let method_overrides = extract_overrides(&method.config);
|
||||
let policy_overrides = extract_overrides(&policy.overrides);
|
||||
let fallible = if policy.overrides.get("fallible").is_some() {
|
||||
policy_overrides.0
|
||||
} else {
|
||||
method_overrides.0
|
||||
};
|
||||
let max_lives = if policy.overrides.get("max_lives").is_some() {
|
||||
policy_overrides.1
|
||||
} else {
|
||||
method_overrides.1
|
||||
};
|
||||
let timeout = if policy.overrides.get("timeout_ms").is_some() {
|
||||
policy_overrides.2
|
||||
} else {
|
||||
method_overrides.2
|
||||
};
|
||||
let retry_backoff = if policy.overrides.get("retry_backoff").is_some() {
|
||||
policy_overrides.3
|
||||
} else {
|
||||
method_overrides.3
|
||||
};
|
||||
method_rules.push(NotificationRule {
|
||||
enabled: true,
|
||||
trigger,
|
||||
template,
|
||||
fallible,
|
||||
max_lives,
|
||||
timeout,
|
||||
config: method.config.clone(),
|
||||
retry_backoff,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ruleset.insert(method_name.clone(), method_rules);
|
||||
}
|
||||
rules.insert(*priority, ruleset);
|
||||
}
|
||||
rules
|
||||
}
|
||||
|
||||
// calculate min(1, k/x)*t as notification grouping period
|
||||
// to avoid excessive notification frequency
|
||||
fn wait_time(group_period: u32, group_watermark: u32, count: u32) -> f32 {
|
||||
@@ -141,16 +368,16 @@ fn notify_delay(event: &NotificationEvent) -> std::time::Duration {
|
||||
event.lives,
|
||||
event.max_lives
|
||||
);
|
||||
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||
return std::time::Duration::from_secs_f64(NOTIFICATION_TEMPORARY_MAX_TIME);
|
||||
}
|
||||
let multiplier = NOTIFY_TEMPORARY_DELAY_FACTOR.powi(exponent);
|
||||
let delay = multiplier * NOTIFY_TEMPORARY_DELAY_TIME;
|
||||
let multiplier = NOTIFICATION_TEMPORARY_DELAY_FACTOR.powi(exponent);
|
||||
let delay = multiplier * NOTIFICATION_TEMPORARY_DELAY_TIME;
|
||||
if !delay.is_finite() || delay < 0.0 {
|
||||
log::warn!(
|
||||
"Unexpected calculated delay: {}, treating as infinite",
|
||||
delay
|
||||
);
|
||||
return std::time::Duration::from_secs_f64(NOTIFY_TEMPORARY_MAX_TIME);
|
||||
return std::time::Duration::from_secs_f64(NOTIFICATION_TEMPORARY_MAX_TIME);
|
||||
}
|
||||
std::time::Duration::from_secs_f64(delay)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::notify::NotificationRenderer;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -16,4 +17,10 @@ pub enum NotifyError {
|
||||
|
||||
#[error("Notification failed after {0} attempt(s)")]
|
||||
Exhausted(u32),
|
||||
|
||||
#[error("Unknown plugin: {0}")]
|
||||
UnknownPlugin(String),
|
||||
|
||||
#[error("Unsupported renderer for {0}: {1}")]
|
||||
UnsupportedRenderer(String, NotificationRenderer),
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use crate::finalize::plugin::FinalizePlugin;
|
||||
use crate::types::buildstatus::StageOutcome;
|
||||
use crate::types::buildstatus::StagePhase;
|
||||
use chrono::{DateTime, Utc};
|
||||
use globset::GlobSet;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use crate::finalize::plugin::FinalizePlugin;
|
||||
use crate::types::buildstatus::StagePhase;
|
||||
use crate::types::buildstatus::StageResult;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::finalize::NotificationTemplate;
|
||||
@@ -15,7 +19,7 @@ use crate::finalize::config::Trigger;
|
||||
pub struct NotificationRule {
|
||||
pub enabled: bool,
|
||||
|
||||
pub trigger: Vec<Trigger>,
|
||||
pub trigger: Vec<ParsedTrigger>,
|
||||
|
||||
pub template: NotificationTemplate,
|
||||
|
||||
@@ -36,11 +40,55 @@ pub struct NotificationRule{
|
||||
pub retry_backoff: f64,
|
||||
}
|
||||
|
||||
/// Parsed notification trigger type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParsedTrigger {
|
||||
OnSuccess,
|
||||
OnFailure,
|
||||
OnFinishOf(Matchable),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Matchable {
|
||||
Glob(GlobSet),
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
impl Matchable {
|
||||
fn matches(&self, stage: &StagePhase, substage: &str) -> bool {
|
||||
let stagename = [stage.as_str(), ".", substage].concat();
|
||||
|
||||
match self {
|
||||
Matchable::Glob(s) => s.is_match(stagename.as_str()),
|
||||
Matchable::Regex(r) => r.is_match(stagename.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedTrigger {
|
||||
pub fn accept(&self, outcome: &StageOutcome, stage: &StagePhase, substage: &str) -> bool {
|
||||
match self {
|
||||
ParsedTrigger::OnSuccess => outcome == &StageOutcome::PipelineSuccess,
|
||||
ParsedTrigger::OnFailure => outcome == &StageOutcome::PipelineFailure,
|
||||
// TODO: Implement condition checking for OnFinishOf
|
||||
ParsedTrigger::OnFinishOf(m) => m.matches(stage, substage),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationRule {
|
||||
pub fn accept(&self, result: &StageOutcome, stage: &StagePhase, substage: &str) -> bool {
|
||||
self.trigger
|
||||
.iter()
|
||||
.any(|t| t.accept(result, stage, substage))
|
||||
}
|
||||
}
|
||||
|
||||
pub type Priority = u32;
|
||||
pub type Method = String;
|
||||
pub type NotificationRulesMap = HashMap<Priority, HashMap<Method, Vec<NotificationRule>>>;
|
||||
pub type BundledNotificationEvent = Vec<NotificationEvent>;
|
||||
pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<BundledNotificationEvent>;
|
||||
// pub type BundledNotificationEvent = Vec<NotificationEvent>;
|
||||
pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver<NotificationEvent>;
|
||||
pub type NotificationEventSender = tokio::sync::mpsc::Sender<NotificationEvent>;
|
||||
pub type NotificationPluginMap = HashMap<String, FinalizePlugin>;
|
||||
|
||||
@@ -49,7 +97,7 @@ pub struct NotificationEvent {
|
||||
pub id: Uuid,
|
||||
pub stage: StagePhase,
|
||||
pub substage: String,
|
||||
pub result: StageResult,
|
||||
pub outcome: StageOutcome,
|
||||
pub not_before: DateTime<Utc>,
|
||||
pub priority: u32,
|
||||
pub effective_priority: u32,
|
||||
@@ -89,8 +137,40 @@ impl Ord for NotificationEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delay before retrying an unavailable plugin (5 seconds)
|
||||
pub const NOTIFICATION_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
pub const NOTIFY_NOTREADY_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
pub const NOTIFY_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
|
||||
pub const NOTIFY_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||
pub const NOTIFY_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
||||
/// Exponential backoff base (2.0 = double each retry)
|
||||
pub const NOTIFICATION_TEMPORARY_DELAY_FACTOR: f64 = 2.0;
|
||||
|
||||
/// Initial backoff delay for temporary errors (5 seconds)
|
||||
pub const NOTIFICATION_TEMPORARY_DELAY_TIME: f64 = 5.0;
|
||||
|
||||
/// Upper bound for backoff delay (unbounded)
|
||||
pub const NOTIFICATION_TEMPORARY_MAX_TIME: f64 = f64::MAX;
|
||||
|
||||
/// Lives recovered after downgrade as fraction of max_lives (0.0 = none)
|
||||
pub const NOTIFICATION_LIFE_RECOVERY_FACTOR: f64 = 0.0;
|
||||
|
||||
pub const NOTIFICATION_DEFAULT_FALLIBLE: bool = false;
|
||||
pub const NOTIFICATION_DEFAULT_MAX_LIVES: u32 = 3;
|
||||
pub const NOTIFICATION_DEFAULT_TIMEOUT_MS: u64 = 30000;
|
||||
pub const NOTIFICATION_DEFAULT_RETRY_BACKOFF: f64 = 2.0;
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Clone)]
|
||||
pub enum NotificationRenderer {
|
||||
#[default]
|
||||
Server,
|
||||
Plugin,
|
||||
Interactive,
|
||||
}
|
||||
|
||||
impl Display for NotificationRenderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NotificationRenderer::Server => write!(f, "server"),
|
||||
NotificationRenderer::Plugin => write!(f, "plugin"),
|
||||
NotificationRenderer::Interactive => write!(f, "interactive"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,20 +21,20 @@
|
||||
//! ```
|
||||
|
||||
pub mod config;
|
||||
pub mod constant;
|
||||
pub mod env;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod security;
|
||||
pub mod stage;
|
||||
pub mod types;
|
||||
|
||||
use crate::{
|
||||
ExecutionContext,
|
||||
cli::Cli,
|
||||
constant::STANDALONE_STATUS_DIR,
|
||||
engine::EventSender,
|
||||
error::PrebakeError,
|
||||
prebake::{security::get_drop_after, stage::PrebakeStage},
|
||||
types::buildstatus::{StageInfo, StagePhase, StageResult, write_stage_status},
|
||||
prebake::{error::PrebakeError, security::get_drop_after, stage::PrebakeStage},
|
||||
};
|
||||
use workshop_engine::EventSender;
|
||||
use chrono::Utc;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -131,26 +131,6 @@ pub async fn prebake(
|
||||
ctx.task_id = format!("{}-{}-bootstrap", cli.pipeline, cli.build_id);
|
||||
let osinfo = os_info::get();
|
||||
let result = stage::bootstrap::bootstrap(&prebake, &osinfo, &ctx, &event_tx).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "bootstrap".to_string(),
|
||||
phase: StagePhase::Prebake,
|
||||
substage: "bootstrap".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
|
||||
@@ -165,26 +145,6 @@ pub async fn prebake(
|
||||
None => None,
|
||||
};
|
||||
let result = stage::hook::hook(earlyhook, &ctx, event_tx.clone()).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "earlyhook".to_string(),
|
||||
phase: StagePhase::Prebake,
|
||||
substage: "earlyhook".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
|
||||
@@ -193,10 +153,10 @@ pub async fn prebake(
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|c| c.repology_endpoint)
|
||||
.unwrap_or(crate::types::repology::RepologyEndpoint::Default);
|
||||
.unwrap_or(workshop_engine::RepologyEndpoint::Default);
|
||||
|
||||
let osinfo = os_info::get();
|
||||
let detected_pm = crate::engine::pm::detect(&osinfo).map(|pm| pm.name().to_string());
|
||||
let detected_pm = workshop_engine::pm::detect(&osinfo).map(|pm| pm.name().to_string());
|
||||
|
||||
let upm_sys_deps = if let Some(user) = &dependencies.user {
|
||||
if stage <= PrebakeStage::DepsSystem {
|
||||
@@ -213,7 +173,7 @@ pub async fn prebake(
|
||||
if !upm_sys_deps.deps.is_empty() {
|
||||
if let Some(pm_name) = &detected_pm {
|
||||
for (upm_name, sys_deps) in &upm_sys_deps.deps {
|
||||
let resolved = crate::engine::repology::resolve_package_names(
|
||||
let resolved = workshop_engine::repology::resolve_package_names(
|
||||
&sys_deps.packages,
|
||||
sys_deps.mapped,
|
||||
&endpoint,
|
||||
@@ -254,26 +214,6 @@ pub async fn prebake(
|
||||
ctx.task_id = format!("{}-{}-depssystem", cli.pipeline, cli.build_id);
|
||||
let result =
|
||||
stage::depssystem::depssystem(&system_config, &ctx, event_tx.clone()).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "depssystem".to_string(),
|
||||
phase: StagePhase::Prebake,
|
||||
substage: "depssystem".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
|
||||
@@ -285,26 +225,6 @@ pub async fn prebake(
|
||||
prebake_drop_privilege(PrebakeStage::DepsUser, dropafter, target_user, &mut ctx)?;
|
||||
ctx.task_id = format!("{}-{}-depsuser", cli.pipeline, cli.build_id);
|
||||
let result = stage::depsuser::depsuser(&user, &ctx, event_tx.clone(), &endpoint).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "depsuser".to_string(),
|
||||
phase: StagePhase::Prebake,
|
||||
substage: "depsuser".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
}
|
||||
@@ -320,26 +240,6 @@ pub async fn prebake(
|
||||
None => None,
|
||||
};
|
||||
let result = stage::hook::hook(latehook, &ctx, event_tx.clone()).await;
|
||||
let stage_result = if result.is_ok() {
|
||||
StageResult::Success
|
||||
} else {
|
||||
StageResult::Failure
|
||||
};
|
||||
let _ = write_stage_status(
|
||||
STANDALONE_STATUS_DIR,
|
||||
StageInfo {
|
||||
name: "latehook".to_string(),
|
||||
phase: StagePhase::Prebake,
|
||||
substage: "latehook".to_string(),
|
||||
result: stage_result,
|
||||
duration_ms: Utc::now()
|
||||
.signed_duration_since(stage_start)
|
||||
.num_milliseconds() as u64,
|
||||
started_at: stage_start,
|
||||
finished_at: Utc::now(),
|
||||
error_message: result.as_ref().err().map(|e| e.to_string()),
|
||||
},
|
||||
);
|
||||
result?;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@ use serde_yaml::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::architecture::Architecture;
|
||||
use crate::types::builderconfig::{
|
||||
use crate::prebake::types::architecture::Architecture;
|
||||
use crate::prebake::types::builderconfig::{
|
||||
BaremetalConfig, BuilderType, CustomConfig, DockerConfig, FirecrackerConfig,
|
||||
};
|
||||
use crate::types::cache::{CacheDirectory, CacheStrategy};
|
||||
use crate::types::command::CustomCommand;
|
||||
use crate::types::memsize::MemSize;
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
use crate::prebake::types::cache::{CacheDirectory, CacheStrategy};
|
||||
use crate::prebake::types::memsize::MemSize;
|
||||
use workshop_engine::{CustomCommand, RepologyEndpoint};
|
||||
|
||||
pub const VERSION_REQUIREMENT: &str = "^1.0";
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh";
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use crate::types::builderconfig::DockerConfig;
|
||||
use crate::prebake::types::builderconfig::DockerConfig;
|
||||
use anyhow::Context;
|
||||
use bollard::{
|
||||
API_DEFAULT_VERSION, Docker, body_full,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
use workshop_engine::{
|
||||
ExecutionError,
|
||||
error::{
|
||||
EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_PRIV_DROP_FAILED,
|
||||
HasExitCode,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PrebakeError {
|
||||
#[error("Privilege drop failed: {0}")]
|
||||
PrivDropFailed(String),
|
||||
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
|
||||
#[error("Invalid privilege state: current_uid={current_uid}, target_uid={target_uid}")]
|
||||
InvalidPrivilegeState { current_uid: u32, target_uid: u32 },
|
||||
|
||||
#[error("Invalid stage: current={current}, expected={expected}")]
|
||||
InvalidStage { current: String, expected: String },
|
||||
|
||||
#[error("Failed to validate prebake.yml.")]
|
||||
ValidateError(),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("YAML parse error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Bad prebake.yml: {0}")]
|
||||
ConfigError(String),
|
||||
}
|
||||
|
||||
impl HasExitCode for PrebakeError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
PrebakeError::PrivDropFailed(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::UserNotFound(_) => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::InvalidPrivilegeState { .. } => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::ValidateError() => EXITCODE_PARSE_ERROR,
|
||||
PrebakeError::InvalidStage { .. } => EXITCODE_PRIV_DROP_FAILED,
|
||||
PrebakeError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
PrebakeError::YamlParseError(_) => EXITCODE_PARSE_ERROR,
|
||||
PrebakeError::ConfigError(_) => EXITCODE_PARSE_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BootstrapError {
|
||||
#[error("Failed to write bootstrap script: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to find bootstrap.sh: {0}")]
|
||||
ScriptNotFound(PathBuf),
|
||||
|
||||
#[error("Failed to execute bootstrap.sh: {0}")]
|
||||
ExecutionError(#[from] ExecutionError),
|
||||
|
||||
#[error("Failed to create user {0}: {1}")]
|
||||
UserCreationFailed(String, String),
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::engine::{ExecutionEvent, StreamType};
|
||||
use workshop_engine::{ExecutionEvent, StreamType};
|
||||
|
||||
pub async fn event_receiver(
|
||||
mut rx: tokio::sync::mpsc::Receiver<ExecutionEvent>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::ExecutionContext;
|
||||
use crate::error::PrebakeError;
|
||||
use crate::prebake::error::PrebakeError;
|
||||
use crate::prebake::config::Security;
|
||||
use crate::prebake::stage::PrebakeStage;
|
||||
use privdrop::PrivDrop;
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
//! and the target operating system, supporting multiple package managers.
|
||||
|
||||
use crate::ExecutionContext;
|
||||
use crate::constant::BOOTSTRAP_SCRIPT_PATH;
|
||||
use crate::engine::{Engine, EventSender, pm};
|
||||
use crate::error::BootstrapError;
|
||||
use crate::prebake::constant::BOOTSTRAP_SCRIPT_PATH;
|
||||
use workshop_engine::{Engine, EventSender, pm};
|
||||
use crate::prebake::error::BootstrapError;
|
||||
use crate::prebake::config::Bootstrap;
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use std::fs::File;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::{Engine, EventSender, pm};
|
||||
use crate::error::DependencyError;
|
||||
use workshop_engine::{Engine, EventSender, pm, error::DependencyError};
|
||||
use crate::prebake::config::SystemDependency;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use serde_yaml::Value;
|
||||
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::{EventSender, upm};
|
||||
use crate::error::DependencyError;
|
||||
use crate::types::repology::RepologyEndpoint;
|
||||
use workshop_engine::{EventSender, upm, error::DependencyError, RepologyEndpoint};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::prebake::config::PrebakeConfig;
|
||||
use crate::types::builderconfig::BuilderType;
|
||||
use crate::prebake::types::builderconfig::BuilderType;
|
||||
use config::Config;
|
||||
|
||||
pub use crate::prebake::env::baremetal::prepare_baremetal;
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
//! and timeout configurations.
|
||||
|
||||
use crate::ExecutionContext;
|
||||
use crate::engine::types::DeltaExecutionContext;
|
||||
use crate::engine::{Engine, EventSender, ExecutionError};
|
||||
use crate::types::command::CustomCommand;
|
||||
use workshop_engine::{DeltaExecutionContext, Engine, EventSender, ExecutionError, CustomCommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Executes a sequence of hook commands.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod architecture;
|
||||
pub mod builderconfig;
|
||||
pub mod cache;
|
||||
pub mod memsize;
|
||||
+61
-16
@@ -70,7 +70,7 @@ impl FromStr for Architecture {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use workshop_baker::types::Architecture;
|
||||
/// # use workshop_baker::prebake::types::architecture::Architecture;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// let arch = Architecture::from_str("amd64").unwrap();
|
||||
@@ -109,7 +109,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalized_loong64_to_loongarch64() {
|
||||
assert_eq!(Architecture::Loong64.normalized(), Architecture::Loongarch64);
|
||||
assert_eq!(
|
||||
Architecture::Loong64.normalized(),
|
||||
Architecture::Loongarch64
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -134,7 +137,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalized_loongarch64_unchanged() {
|
||||
assert_eq!(Architecture::Loongarch64.normalized(), Architecture::Loongarch64);
|
||||
assert_eq!(
|
||||
Architecture::Loongarch64.normalized(),
|
||||
Architecture::Loongarch64
|
||||
);
|
||||
}
|
||||
|
||||
// as_str() tests
|
||||
@@ -155,27 +161,66 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_from_str_valid_canonical() {
|
||||
assert_eq!("noarch".parse::<Architecture>().unwrap(), Architecture::NoArch);
|
||||
assert_eq!("x86_64".parse::<Architecture>().unwrap(), Architecture::Amd64);
|
||||
assert_eq!("arm64".parse::<Architecture>().unwrap(), Architecture::Aarch64);
|
||||
assert_eq!("riscv64".parse::<Architecture>().unwrap(), Architecture::Riscv64);
|
||||
assert_eq!("loongarch64".parse::<Architecture>().unwrap(), Architecture::Loong64);
|
||||
assert_eq!(
|
||||
"noarch".parse::<Architecture>().unwrap(),
|
||||
Architecture::NoArch
|
||||
);
|
||||
assert_eq!(
|
||||
"x86_64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Amd64
|
||||
);
|
||||
assert_eq!(
|
||||
"arm64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Aarch64
|
||||
);
|
||||
assert_eq!(
|
||||
"riscv64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Riscv64
|
||||
);
|
||||
assert_eq!(
|
||||
"loongarch64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Loong64
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_valid_aliases() {
|
||||
assert_eq!("amd64".parse::<Architecture>().unwrap(), Architecture::Amd64);
|
||||
assert_eq!("aarch64".parse::<Architecture>().unwrap(), Architecture::Aarch64);
|
||||
assert_eq!("loong64".parse::<Architecture>().unwrap(), Architecture::Loong64);
|
||||
assert_eq!(
|
||||
"amd64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Amd64
|
||||
);
|
||||
assert_eq!(
|
||||
"aarch64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Aarch64
|
||||
);
|
||||
assert_eq!(
|
||||
"loong64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Loong64
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_case_insensitive() {
|
||||
assert_eq!("X86_64".parse::<Architecture>().unwrap(), Architecture::Amd64);
|
||||
assert_eq!("ARM64".parse::<Architecture>().unwrap(), Architecture::Aarch64);
|
||||
assert_eq!("RISCV64".parse::<Architecture>().unwrap(), Architecture::Riscv64);
|
||||
assert_eq!("NoArch".parse::<Architecture>().unwrap(), Architecture::NoArch);
|
||||
assert_eq!("LoongArch64".parse::<Architecture>().unwrap(), Architecture::Loong64);
|
||||
assert_eq!(
|
||||
"X86_64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Amd64
|
||||
);
|
||||
assert_eq!(
|
||||
"ARM64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Aarch64
|
||||
);
|
||||
assert_eq!(
|
||||
"RISCV64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Riscv64
|
||||
);
|
||||
assert_eq!(
|
||||
"NoArch".parse::<Architecture>().unwrap(),
|
||||
Architecture::NoArch
|
||||
);
|
||||
assert_eq!(
|
||||
"LoongArch64".parse::<Architecture>().unwrap(),
|
||||
Architecture::Loong64
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
+4
-1
@@ -92,7 +92,10 @@ mod tests {
|
||||
let config = DockerConfig {
|
||||
image: Some("rust:latest".to_string()),
|
||||
dockerfile: Some("Dockerfile".to_string()),
|
||||
build_args: Some(HashMap::from([("RUST_BACKTRACE".to_string(), "1".to_string())])),
|
||||
build_args: Some(HashMap::from([(
|
||||
"RUST_BACKTRACE".to_string(),
|
||||
"1".to_string(),
|
||||
)])),
|
||||
};
|
||||
let yaml = serde_yaml::to_string(&config).unwrap();
|
||||
let deserialized: DockerConfig = serde_yaml::from_str(&yaml).unwrap();
|
||||
@@ -1,9 +1 @@
|
||||
pub mod architecture;
|
||||
pub mod builderconfig;
|
||||
pub mod buildstatus;
|
||||
pub mod cache;
|
||||
pub mod command;
|
||||
pub mod compression;
|
||||
pub mod memsize;
|
||||
pub mod repology;
|
||||
pub mod time;
|
||||
|
||||
@@ -22,9 +22,19 @@ impl std::fmt::Display for StagePhase {
|
||||
}
|
||||
}
|
||||
|
||||
impl StagePhase {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
StagePhase::Prebake => "prebake",
|
||||
StagePhase::Bake => "bake",
|
||||
StagePhase::Finalize => "finalize",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a pipeline stage execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum StageResult {
|
||||
pub enum StageOutcome {
|
||||
/// Stage completed successfully.
|
||||
Success,
|
||||
/// Stage failed during execution.
|
||||
@@ -33,15 +43,21 @@ pub enum StageResult {
|
||||
Canceled,
|
||||
/// Stage was skipped.
|
||||
Skipped,
|
||||
/// Pipeline succeeded.
|
||||
PipelineSuccess,
|
||||
/// Pipeline failed.
|
||||
PipelineFailure,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StageResult {
|
||||
impl std::fmt::Display for StageOutcome {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StageResult::Success => write!(f, "success"),
|
||||
StageResult::Failure => write!(f, "failure"),
|
||||
StageResult::Canceled => write!(f, "canceled"),
|
||||
StageResult::Skipped => write!(f, "skipped"),
|
||||
StageOutcome::Success => write!(f, "success"),
|
||||
StageOutcome::Failure => write!(f, "failure"),
|
||||
StageOutcome::Canceled => write!(f, "canceled"),
|
||||
StageOutcome::Skipped => write!(f, "skipped"),
|
||||
StageOutcome::PipelineSuccess => write!(f, "pipeline-success"),
|
||||
StageOutcome::PipelineFailure => write!(f, "pipeline-failure"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +72,7 @@ pub struct StageInfo {
|
||||
/// Substage identifier within the phase.
|
||||
pub substage: String,
|
||||
/// Stage outcome.
|
||||
pub result: StageResult,
|
||||
pub result: StageOutcome,
|
||||
/// Execution duration in milliseconds.
|
||||
pub duration_ms: u64,
|
||||
/// Stage start timestamp.
|
||||
@@ -100,18 +116,18 @@ pub struct BuildStatus {
|
||||
|
||||
impl BuildStatus {
|
||||
/// Returns the result of the final stage, or `Success` if no stages recorded.
|
||||
pub fn final_result(&self) -> StageResult {
|
||||
pub fn final_result(&self) -> StageOutcome {
|
||||
self.stages
|
||||
.last()
|
||||
.map(|s| s.result.clone())
|
||||
.unwrap_or(StageResult::Success)
|
||||
.unwrap_or(StageOutcome::Success)
|
||||
}
|
||||
|
||||
/// Returns `true` if any stage failed.
|
||||
pub fn has_failures(&self) -> bool {
|
||||
self.stages
|
||||
.iter()
|
||||
.any(|s| matches!(s.result, StageResult::Failure))
|
||||
.any(|s| matches!(s.result, StageOutcome::Failure))
|
||||
}
|
||||
|
||||
/// Returns the full name of the last stage, or `None` if no stages recorded.
|
||||
@@ -120,75 +136,11 @@ impl BuildStatus {
|
||||
}
|
||||
}
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
/// Appends a stage record to the status file in `status_dir`.
|
||||
///
|
||||
/// Each line in the file contains a JSON-encoded `StageInfo` entry.
|
||||
/// Creates the file if it does not exist, appends otherwise.
|
||||
pub fn write_stage_status<P: AsRef<Path>>(status_dir: P, stage: StageInfo) -> io::Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
let status_file = status_dir
|
||||
.as_ref()
|
||||
.join(crate::constant::STANDALONE_STATUS_FILE);
|
||||
let line = serde_json::to_string(&stage).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("JSON serialization error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&status_file)?;
|
||||
|
||||
writeln!(file, "{}", line)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads and parses the build status from `status_dir`.
|
||||
///
|
||||
/// Reads the status file, parsing each line as a JSON-encoded `StageInfo`.
|
||||
/// Returns an empty `BuildStatus` if the file does not exist.
|
||||
/// Empty lines are ignored during parsing.
|
||||
pub fn read_build_status<P: AsRef<Path>>(status_dir: P) -> io::Result<BuildStatus> {
|
||||
use std::fs;
|
||||
|
||||
let status_file = status_dir
|
||||
.as_ref()
|
||||
.join(crate::constant::STANDALONE_STATUS_FILE);
|
||||
|
||||
if !status_file.exists() {
|
||||
return Ok(BuildStatus::default());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&status_file)?;
|
||||
let stages: Vec<StageInfo> = content
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| {
|
||||
serde_json::from_str(line).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("JSON deserialization error: {}", e),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(BuildStatus { stages })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_test_stage(name: &str, phase: StagePhase, result: StageResult) -> StageInfo {
|
||||
fn create_test_stage(name: &str, phase: StagePhase, result: StageOutcome) -> StageInfo {
|
||||
let now = Utc::now();
|
||||
StageInfo {
|
||||
name: name.to_string(),
|
||||
@@ -202,37 +154,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_and_read_stage_status() {
|
||||
let dir = tempdir().unwrap();
|
||||
let stage = create_test_stage("bootstrap", StagePhase::Prebake, StageResult::Success);
|
||||
|
||||
write_stage_status(&dir, stage.clone()).unwrap();
|
||||
|
||||
let status = read_build_status(&dir).unwrap();
|
||||
assert_eq!(status.stages.len(), 1);
|
||||
assert_eq!(status.stages[0].substage, "bootstrap");
|
||||
assert_eq!(status.stages[0].result, StageResult::Success);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_status_final_result() {
|
||||
let status = BuildStatus {
|
||||
stages: vec![
|
||||
create_test_stage("bootstrap", StagePhase::Prebake, StageResult::Success),
|
||||
create_test_stage("build", StagePhase::Bake, StageResult::Failure),
|
||||
create_test_stage("bootstrap", StagePhase::Prebake, StageOutcome::Success),
|
||||
create_test_stage("build", StagePhase::Bake, StageOutcome::Failure),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(status.final_result(), StageResult::Failure);
|
||||
assert_eq!(status.final_result(), StageOutcome::Failure);
|
||||
assert!(status.has_failures());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_empty_status() {
|
||||
let dir = tempdir().unwrap();
|
||||
let status = read_build_status(&dir).unwrap();
|
||||
assert!(status.stages.is_empty());
|
||||
assert_eq!(status.final_result(), StageResult::Success);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use workshop_baker::types::architecture::Architecture;
|
||||
use workshop_baker::types::builderconfig::BuilderConfig;
|
||||
use workshop_baker::types::cache::{CacheDirectory, CacheMode, CacheStrategyType};
|
||||
use workshop_baker::types::compression::CompressionMethod;
|
||||
use workshop_baker::types::command::CustomCommand;
|
||||
use workshop_baker::types::compression::CompressionMethod;
|
||||
use workshop_baker::types::repology::RepologyEndpoint;
|
||||
|
||||
#[test]
|
||||
|
||||
Generated
+885
@@ -0,0 +1,885 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "duration-str"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "027cd1402a609c71a9ac333c7e3d90ee042e7a131da1a83da8c60df323f12f61"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"time",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-image"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-location"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-location",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"objc2-user-notifications",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-user-notifications"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"serde",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "workshop-engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"duration-str",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"os_info",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "workshop-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "workshop_engine"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
thiserror = "2.0.17"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.148"
|
||||
serde_yaml = "0.9.34"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
async-trait = "0.1.87"
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.28"
|
||||
os_info = "3.14.0"
|
||||
duration-str = "0.21.0"
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::types::time::deserialize_duration_ms;
|
||||
use crate::time::deserialize_duration_ms;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
pub trait HasExitCode: Display + Debug + Send + Sync + Sized + 'static {
|
||||
fn exit_code(&self) -> i32;
|
||||
}
|
||||
|
||||
pub const EXITCODE_OK: i32 = 0;
|
||||
pub const EXITCODE_GENERAL_ERROR: i32 = 1;
|
||||
pub const EXITCODE_INVALID_ARGS: i32 = 2;
|
||||
pub const EXITCODE_IO_ERROR: i32 = 3;
|
||||
pub const EXITCODE_PARSE_ERROR: i32 = 4;
|
||||
pub const EXITCODE_EXECUTION_ERROR: i32 = 5;
|
||||
pub const EXITCODE_TIMEOUT: i32 = 124;
|
||||
pub const EXITCODE_PRIV_DROP_FAILED: i32 = 201;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExecutionError {
|
||||
#[error("Invalid command: {0}")]
|
||||
InvalidCommand(String),
|
||||
|
||||
#[error("Execution failed: {0}")]
|
||||
ExecutionFailed(String),
|
||||
|
||||
#[error("Permission denied: uid={euid}, gid={egid}: {reason}")]
|
||||
PermissionDenied {
|
||||
euid: u32,
|
||||
egid: u32,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(String),
|
||||
|
||||
#[error("Script not found: {0}")]
|
||||
ScriptNotFound(PathBuf),
|
||||
|
||||
#[error("Package manager not found")]
|
||||
PackageManagerNotFound,
|
||||
}
|
||||
|
||||
impl HasExitCode for ExecutionError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
ExecutionError::InvalidCommand(_) => EXITCODE_EXECUTION_ERROR,
|
||||
ExecutionError::ExecutionFailed(_) => EXITCODE_EXECUTION_ERROR,
|
||||
ExecutionError::PermissionDenied { .. } => EXITCODE_GENERAL_ERROR,
|
||||
ExecutionError::Timeout => EXITCODE_TIMEOUT,
|
||||
ExecutionError::IoError(_) => EXITCODE_IO_ERROR,
|
||||
ExecutionError::ScriptNotFound(_) => EXITCODE_INVALID_ARGS,
|
||||
ExecutionError::PackageManagerNotFound => EXITCODE_GENERAL_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DependencyError {
|
||||
#[error("No supported package manager found for {0}")]
|
||||
UnsupportedPlatform(os_info::Info),
|
||||
|
||||
#[error("Unknown package manager: {0}")]
|
||||
UnknownManager(String),
|
||||
|
||||
#[error("Failed to parse config")]
|
||||
ParseError(),
|
||||
|
||||
#[error("Stage {stage} failed: {source}")]
|
||||
StageFailed {
|
||||
stage: &'static str,
|
||||
#[source]
|
||||
source: ExecutionError,
|
||||
},
|
||||
|
||||
#[error("Custom command execution failed: {0}")]
|
||||
CustomFailed(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
impl HasExitCode for DependencyError {
|
||||
fn exit_code(&self) -> i32 {
|
||||
// DependencyError doesn't need custom exit codes; use generic
|
||||
1
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@ use tokio::process::Command;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::pm::PackageManager;
|
||||
use super::types::{
|
||||
use crate::pm::PackageManager;
|
||||
use crate::types::{
|
||||
CgroupManager, DeltaExecutionContext, EventSender, ExecutionContext, ExecutionError,
|
||||
ExecutionEvent, ExecutionResult, ResourceLimits, StreamType,
|
||||
};
|
||||
|
||||
impl super::types::Engine {
|
||||
impl crate::types::Engine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cgroup_manager: None,
|
||||
@@ -367,7 +367,7 @@ impl super::types::Engine {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for super::types::Engine {
|
||||
impl Default for crate::types::Engine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
pub mod cgroups;
|
||||
pub mod command;
|
||||
pub mod error;
|
||||
pub mod executor;
|
||||
pub mod pm;
|
||||
pub mod repology;
|
||||
pub mod time;
|
||||
pub mod types;
|
||||
pub mod upm;
|
||||
|
||||
// Convenience re-exports
|
||||
pub use error::{ExecutionError, HasExitCode,
|
||||
EXITCODE_OK, EXITCODE_GENERAL_ERROR, EXITCODE_INVALID_ARGS,
|
||||
EXITCODE_IO_ERROR, EXITCODE_PARSE_ERROR, EXITCODE_EXECUTION_ERROR,
|
||||
EXITCODE_TIMEOUT, EXITCODE_PRIV_DROP_FAILED};
|
||||
pub use types::{Engine, EventSender, ExecutionContext, ExecutionEvent, ExecutionResult,
|
||||
StreamType, ResourceLimits, ResourceUsage, DeltaExecutionContext};
|
||||
pub use command::CustomCommand;
|
||||
pub use repology::RepologyEndpoint;
|
||||
pub use time::{parse_duration_to_ms, deserialize_duration_ms, deserialize_option_duration_ms};
|
||||
pub use pm::PackageManager;
|
||||
pub use upm::UserPackageManager;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::engine::pm::PackageManager;
|
||||
use crate::pm::PackageManager;
|
||||
use os_info::{Info, Version};
|
||||
use serde_yaml::Value;
|
||||
use tokio::process::Command;
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::PackageManager;
|
||||
use crate::pm::PackageManager;
|
||||
use os_info::{Info, Type};
|
||||
use serde::Deserialize;
|
||||
use serde_yaml::Value;
|
||||
@@ -1,3 +1,7 @@
|
||||
pub mod local;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Repology endpoint configuration for package name resolution.
|
||||
///
|
||||
/// # Deserialize
|
||||
@@ -81,6 +85,48 @@ impl serde::Serialize for RepologyEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RepologyError {
|
||||
Unimplemented,
|
||||
RemoteNotSupported,
|
||||
}
|
||||
|
||||
pub async fn resolve_package_names(
|
||||
packages: &[String],
|
||||
mapped: bool,
|
||||
endpoint: &RepologyEndpoint,
|
||||
target_pm: &str,
|
||||
) -> Result<HashMap<String, Vec<String>>, RepologyError> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
if packages.is_empty() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
if mapped {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
match endpoint {
|
||||
RepologyEndpoint::Local | RepologyEndpoint::Default => {
|
||||
let resolved = local::resolve(packages, target_pm).await;
|
||||
if !resolved.is_empty() {
|
||||
result.insert(target_pm.to_string(), resolved);
|
||||
}
|
||||
}
|
||||
RepologyEndpoint::None => {
|
||||
result.insert(target_pm.to_string(), packages.to_vec());
|
||||
}
|
||||
RepologyEndpoint::Disabled => {}
|
||||
RepologyEndpoint::Remote | RepologyEndpoint::Server => {
|
||||
return Err(RepologyError::Unimplemented);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -16,7 +16,7 @@ pub const INT_MAX_MS: u64 = i32::MAX as u64;
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use workshop_baker::types::time::parse_duration_to_ms;
|
||||
/// # use workshop_engine::time::parse_duration_to_ms;
|
||||
/// assert_eq!(parse_duration_to_ms("30s").unwrap(), 30_000);
|
||||
/// assert_eq!(parse_duration_to_ms("5m").unwrap(), 300_000);
|
||||
/// assert_eq!(parse_duration_to_ms("1h").unwrap(), 3_600_000);
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub use super::cgroups::CgroupManager;
|
||||
pub use crate::cgroups::CgroupManager;
|
||||
pub use crate::error::{EXITCODE_EXECUTION_ERROR, EXITCODE_IO_ERROR, ExecutionError};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::types::EventSender;
|
||||
use crate::types::EventSender;
|
||||
use async_trait::async_trait;
|
||||
use serde_yaml::Value;
|
||||
mod custom;
|
||||
|
||||
use crate::{ExecutionContext, error::DependencyError};
|
||||
use crate::types::ExecutionContext;
|
||||
use crate::error::DependencyError;
|
||||
|
||||
/// System dependencies required by a user package manager.
|
||||
pub struct UPMSysDeps {
|
||||
@@ -2,11 +2,11 @@ use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::Engine;
|
||||
use crate::engine::types::DeltaExecutionContext;
|
||||
use crate::engine::upm::UPMSysDeps;
|
||||
use crate::types::Engine;
|
||||
use crate::types::DeltaExecutionContext;
|
||||
use crate::upm::UPMSysDeps;
|
||||
use crate::error::DependencyError;
|
||||
use crate::types::command::CustomCommand;
|
||||
use crate::command::CustomCommand;
|
||||
|
||||
use super::UserPackageManager;
|
||||
|
||||
@@ -30,8 +30,8 @@ impl UserPackageManager for Custom {
|
||||
async fn main(
|
||||
&self,
|
||||
config: &serde_yaml::Value,
|
||||
ctx: &crate::ExecutionContext,
|
||||
event_tx: &crate::EventSender,
|
||||
ctx: &crate::types::ExecutionContext,
|
||||
event_tx: &crate::types::EventSender,
|
||||
) -> Result<(), DependencyError> {
|
||||
let custom_config: CustomConfig = match serde_yaml::from_value(config.clone()) {
|
||||
Ok(r) => r,
|
||||
Reference in New Issue
Block a user