From 8fd807f174727d854747db87a7f5af76e53ec50a Mon Sep 17 00:00:00 2001 From: Catty Steve <4795515+Catty2014@user.noreply.gitee.com> Date: Sun, 26 Apr 2026 23:07:08 +0800 Subject: [PATCH] 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. --- 2026-04-25.md | 192 ---- AGENTS.md | 1 + diff.patch | 117 --- workshop-baker/Cargo.lock | 32 + workshop-baker/Cargo.toml | 2 + workshop-baker/src/bake.rs | 55 +- workshop-baker/src/bake/builder.rs | 6 +- workshop-baker/src/bake/constant.rs | 4 + workshop-baker/src/bake/decorator.rs | 2 +- workshop-baker/src/bake/error.rs | 55 ++ workshop-baker/src/bake/parser.rs | 2 +- workshop-baker/src/bake/schedule.rs | 2 +- workshop-baker/src/constant.rs | 14 - workshop-baker/src/engine.rs | 17 - workshop-baker/src/engine/AGENTS.md | 46 - workshop-baker/src/engine/repology.rs | 46 - workshop-baker/src/error.rs | 186 ---- workshop-baker/src/finalize.rs | 34 +- workshop-baker/src/finalize/config.rs | 7 +- workshop-baker/src/finalize/constant.rs | 6 + workshop-baker/src/finalize/error.rs | 2 +- workshop-baker/src/finalize/event.rs | 4 +- workshop-baker/src/finalize/plugin.rs | 4 +- .../src/finalize/plugin/fetch/checksum.rs | 10 +- .../src/finalize/plugin/fetch/git.rs | 10 +- .../finalize/plugin/internal/mail/config.rs | 12 +- .../finalize/plugin/internal/mail/template.rs | 2 +- .../src/finalize/plugin/metadata.rs | 10 +- workshop-baker/src/finalize/plugin/shell.rs | 2 +- workshop-baker/src/finalize/stage/hook.rs | 4 +- workshop-baker/src/finalize/types.rs | 1 + .../src/{ => finalize}/types/compression.rs | 2 +- workshop-baker/src/lib.rs | 7 +- workshop-baker/src/main.rs | 7 +- workshop-baker/src/notify.rs | 251 ++++- workshop-baker/src/notify/error.rs | 7 + workshop-baker/src/notify/types.rs | 110 ++- workshop-baker/src/prebake.rs | 116 +-- workshop-baker/src/prebake/config.rs | 11 +- workshop-baker/src/prebake/constant.rs | 1 + workshop-baker/src/prebake/env/container.rs | 2 +- workshop-baker/src/prebake/error.rs | 67 ++ workshop-baker/src/prebake/event.rs | 2 +- workshop-baker/src/prebake/security.rs | 2 +- workshop-baker/src/prebake/stage/bootstrap.rs | 6 +- .../src/prebake/stage/depssystem.rs | 3 +- workshop-baker/src/prebake/stage/depsuser.rs | 4 +- .../src/prebake/stage/environment.rs | 2 +- workshop-baker/src/prebake/stage/hook.rs | 4 +- workshop-baker/src/prebake/types.rs | 4 + .../src/{ => prebake}/types/architecture.rs | 79 +- .../src/{ => prebake}/types/builderconfig.rs | 7 +- .../src/{ => prebake}/types/cache.rs | 2 +- .../src/{ => prebake}/types/memsize.rs | 0 workshop-baker/src/types.rs | 10 +- workshop-baker/src/types/buildstatus.rs | 131 +-- .../tests/bake_pipeline_integration.rs | 2 +- .../tests/prebake_config_integration.rs | 2 +- .../tests/types_config_integration.rs | 4 +- workshop-engine/Cargo.lock | 885 ++++++++++++++++++ workshop-engine/Cargo.toml | 21 + .../engine => workshop-engine/src}/cgroups.rs | 0 .../types => workshop-engine/src}/command.rs | 4 +- workshop-engine/src/error.rs | 88 ++ .../src}/executor.rs | 8 +- workshop-engine/src/lib.rs | 22 + .../src/engine => workshop-engine/src}/pm.rs | 0 .../engine => workshop-engine/src}/pm/apt.rs | 2 +- .../src}/pm/pacman.rs | 2 +- .../types => workshop-engine/src}/repology.rs | 48 +- .../src}/repology/local.rs | 0 .../src/types => workshop-engine/src}/time.rs | 4 +- .../engine => workshop-engine/src}/types.rs | 2 +- .../src/engine => workshop-engine/src}/upm.rs | 5 +- .../src}/upm/custom.rs | 12 +- 75 files changed, 1791 insertions(+), 1044 deletions(-) delete mode 100644 2026-04-25.md delete mode 100644 diff.patch create mode 100644 workshop-baker/src/bake/constant.rs create mode 100644 workshop-baker/src/bake/error.rs delete mode 100644 workshop-baker/src/constant.rs delete mode 100644 workshop-baker/src/engine.rs delete mode 100644 workshop-baker/src/engine/AGENTS.md delete mode 100644 workshop-baker/src/engine/repology.rs delete mode 100644 workshop-baker/src/error.rs create mode 100644 workshop-baker/src/finalize/constant.rs create mode 100644 workshop-baker/src/finalize/types.rs rename workshop-baker/src/{ => finalize}/types/compression.rs (99%) create mode 100644 workshop-baker/src/prebake/constant.rs create mode 100644 workshop-baker/src/prebake/error.rs create mode 100644 workshop-baker/src/prebake/types.rs rename workshop-baker/src/{ => prebake}/types/architecture.rs (72%) rename workshop-baker/src/{ => prebake}/types/builderconfig.rs (96%) rename workshop-baker/src/{ => prebake}/types/cache.rs (99%) rename workshop-baker/src/{ => prebake}/types/memsize.rs (100%) create mode 100644 workshop-engine/Cargo.lock create mode 100644 workshop-engine/Cargo.toml rename {workshop-baker/src/engine => workshop-engine/src}/cgroups.rs (100%) rename {workshop-baker/src/types => workshop-engine/src}/command.rs (98%) create mode 100644 workshop-engine/src/error.rs rename {workshop-baker/src/engine => workshop-engine/src}/executor.rs (99%) create mode 100644 workshop-engine/src/lib.rs rename {workshop-baker/src/engine => workshop-engine/src}/pm.rs (100%) rename {workshop-baker/src/engine => workshop-engine/src}/pm/apt.rs (98%) rename {workshop-baker/src/engine => workshop-engine/src}/pm/pacman.rs (99%) rename {workshop-baker/src/types => workshop-engine/src}/repology.rs (80%) rename {workshop-baker/src/engine => workshop-engine/src}/repology/local.rs (100%) rename {workshop-baker/src/types => workshop-engine/src}/time.rs (99%) rename {workshop-baker/src/engine => workshop-engine/src}/types.rs (98%) rename {workshop-baker/src/engine => workshop-engine/src}/upm.rs (95%) rename {workshop-baker/src/engine => workshop-engine/src}/upm/custom.rs (88%) diff --git a/2026-04-25.md b/2026-04-25.md deleted file mode 100644 index d432c9c..0000000 --- a/2026-04-25.md +++ /dev/null @@ -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`/`Option` 测试 - -### 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` diff --git a/AGENTS.md b/AGENTS.md index 60de0f4..4de0c0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/diff.patch b/diff.patch deleted file mode 100644 index 37fa035..0000000 --- a/diff.patch +++ /dev/null @@ -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>>; --pub type BundledNotifyEvent = Vec; --pub type NotifyEventReceiver = tokio::sync::mpsc::Receiver; --pub type NotifyEventSender = tokio::sync::mpsc::Sender; --pub type NotifyPluginMap = HashMap; -+pub type BundledNotificationEvent = Vec; -+pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver; -+pub type NotificationEventSender = tokio::sync::mpsc::Sender; -+pub type NotificationPluginMap = HashMap; - - #[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, // Target audience - } - --impl Hash for NotifyEvent { -+impl Hash for NotificationEvent { - fn hash(&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 { - 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() - } diff --git a/workshop-baker/Cargo.lock b/workshop-baker/Cargo.lock index 8c8647b..4e3bacc 100644 --- a/workshop-baker/Cargo.lock +++ b/workshop-baker/Cargo.lock @@ -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" diff --git a/workshop-baker/Cargo.toml b/workshop-baker/Cargo.toml index 71586ea..3cfd082 100644 --- a/workshop-baker/Cargo.toml +++ b/workshop-baker/Cargo.toml @@ -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" diff --git a/workshop-baker/src/bake.rs b/workshop-baker/src/bake.rs index b3a3032..e36e435 100644 --- a/workshop-baker/src/bake.rs +++ b/workshop-baker/src/bake.rs @@ -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?; } } diff --git a/workshop-baker/src/bake/builder.rs b/workshop-baker/src/bake/builder.rs index 0b90aea..d3889f7 100644 --- a/workshop-baker/src/bake/builder.rs +++ b/workshop-baker/src/bake/builder.rs @@ -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 }}"; diff --git a/workshop-baker/src/bake/constant.rs b/workshop-baker/src/bake/constant.rs new file mode 100644 index 0000000..316dff8 --- /dev/null +++ b/workshop-baker/src/bake/constant.rs @@ -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; diff --git a/workshop-baker/src/bake/decorator.rs b/workshop-baker/src/bake/decorator.rs index 6c85ea8..79d4002 100644 --- a/workshop-baker/src/bake/decorator.rs +++ b/workshop-baker/src/bake/decorator.rs @@ -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)" diff --git a/workshop-baker/src/bake/error.rs b/workshop-baker/src/bake/error.rs new file mode 100644 index 0000000..e8b7e7c --- /dev/null +++ b/workshop-baker/src/bake/error.rs @@ -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), + + #[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(), + } + } +} diff --git a/workshop-baker/src/bake/parser.rs b/workshop-baker/src/bake/parser.rs index f617dfd..36c7ca8 100644 --- a/workshop-baker/src/bake/parser.rs +++ b/workshop-baker/src/bake/parser.rs @@ -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; diff --git a/workshop-baker/src/bake/schedule.rs b/workshop-baker/src/bake/schedule.rs index 42692ac..1a28a10 100644 --- a/workshop-baker/src/bake/schedule.rs +++ b/workshop-baker/src/bake/schedule.rs @@ -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; diff --git a/workshop-baker/src/constant.rs b/workshop-baker/src/constant.rs deleted file mode 100644 index f0e6c3c..0000000 --- a/workshop-baker/src/constant.rs +++ /dev/null @@ -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"; diff --git a/workshop-baker/src/engine.rs b/workshop-baker/src/engine.rs deleted file mode 100644 index e479587..0000000 --- a/workshop-baker/src/engine.rs +++ /dev/null @@ -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, -}; diff --git a/workshop-baker/src/engine/AGENTS.md b/workshop-baker/src/engine/AGENTS.md deleted file mode 100644 index a6c1117..0000000 --- a/workshop-baker/src/engine/AGENTS.md +++ /dev/null @@ -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 diff --git a/workshop-baker/src/engine/repology.rs b/workshop-baker/src/engine/repology.rs deleted file mode 100644 index 69da8a7..0000000 --- a/workshop-baker/src/engine/repology.rs +++ /dev/null @@ -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>, 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) -} diff --git a/workshop-baker/src/error.rs b/workshop-baker/src/error.rs deleted file mode 100644 index de5d5fc..0000000 --- a/workshop-baker/src/error.rs +++ /dev/null @@ -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), - - #[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, - } - } -} diff --git a/workshop-baker/src/finalize.rs b/workshop-baker/src/finalize.rs index 438ebb0..7498d7e 100644 --- a/workshop-baker/src/finalize.rs +++ b/workshop-baker/src/finalize.rs @@ -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 { 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(()) } diff --git a/workshop-baker/src/finalize/config.rs b/workshop-baker/src/finalize/config.rs index 6ef8746..90a9a6b 100644 --- a/workshop-baker/src/finalize/config.rs +++ b/workshop-baker/src/finalize/config.rs @@ -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, diff --git a/workshop-baker/src/finalize/constant.rs b/workshop-baker/src/finalize/constant.rs new file mode 100644 index 0000000..6e3831f --- /dev/null +++ b/workshop-baker/src/finalize/constant.rs @@ -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"; diff --git a/workshop-baker/src/finalize/error.rs b/workshop-baker/src/finalize/error.rs index 3c0cf98..d0937e8 100644 --- a/workshop-baker/src/finalize/error.rs +++ b/workshop-baker/src/finalize/error.rs @@ -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, diff --git a/workshop-baker/src/finalize/event.rs b/workshop-baker/src/finalize/event.rs index 6d7bb6e..3080de5 100644 --- a/workshop-baker/src/finalize/event.rs +++ b/workshop-baker/src/finalize/event.rs @@ -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) @@ -50,4 +50,4 @@ pub async fn event_receiver( } } } -} \ No newline at end of file +} diff --git a/workshop-baker/src/finalize/plugin.rs b/workshop-baker/src/finalize/plugin.rs index b85da24..57080fa 100644 --- a/workshop-baker/src/finalize/plugin.rs +++ b/workshop-baker/src/finalize/plugin.rs @@ -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, } diff --git a/workshop-baker/src/finalize/plugin/fetch/checksum.rs b/workshop-baker/src/finalize/plugin/fetch/checksum.rs index dd4a8c6..4c349e8 100644 --- a/workshop-baker/src/finalize/plugin/fetch/checksum.rs +++ b/workshop-baker/src/finalize/plugin/fetch/checksum.rs @@ -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] diff --git a/workshop-baker/src/finalize/plugin/fetch/git.rs b/workshop-baker/src/finalize/plugin/fetch/git.rs index dacb593..0f12ec1 100644 --- a/workshop-baker/src/finalize/plugin/fetch/git.rs +++ b/workshop-baker/src/finalize/plugin/fetch/git.rs @@ -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] diff --git a/workshop-baker/src/finalize/plugin/internal/mail/config.rs b/workshop-baker/src/finalize/plugin/internal/mail/config.rs index d0dc9a0..cf4bc2a 100644 --- a/workshop-baker/src/finalize/plugin/internal/mail/config.rs +++ b/workshop-baker/src/finalize/plugin/internal/mail/config.rs @@ -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] @@ -483,4 +487,4 @@ mod tests { let t: Test = serde_json::from_str(json).unwrap(); assert!(matches!(t.schema, Schema::Custom)); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/finalize/plugin/internal/mail/template.rs b/workshop-baker/src/finalize/plugin/internal/mail/template.rs index 351ff05..aef66fc 100644 --- a/workshop-baker/src/finalize/plugin/internal/mail/template.rs +++ b/workshop-baker/src/finalize/plugin/internal/mail/template.rs @@ -98,4 +98,4 @@ mod tests { let result = render_template(template, &data); assert!(result.is_err()); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/finalize/plugin/metadata.rs b/workshop-baker/src/finalize/plugin/metadata.rs index 2013b44..090dcc5 100644 --- a/workshop-baker/src/finalize/plugin/metadata.rs +++ b/workshop-baker/src/finalize/plugin/metadata.rs @@ -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, + /// 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); @@ -114,4 +122,4 @@ plugin_type: Shell assert_eq!(meta.entrypoint, "./bin/test"); assert_eq!(meta.plugin_type, PluginType::Shell); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/finalize/plugin/shell.rs b/workshop-baker/src/finalize/plugin/shell.rs index 0926969..ca5c35d 100644 --- a/workshop-baker/src/finalize/plugin/shell.rs +++ b/workshop-baker/src/finalize/plugin/shell.rs @@ -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}; diff --git a/workshop-baker/src/finalize/stage/hook.rs b/workshop-baker/src/finalize/stage/hook.rs index 55ebcd0..c723e6f 100644 --- a/workshop-baker/src/finalize/stage/hook.rs +++ b/workshop-baker/src/finalize/stage/hook.rs @@ -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. diff --git a/workshop-baker/src/finalize/types.rs b/workshop-baker/src/finalize/types.rs new file mode 100644 index 0000000..376ef62 --- /dev/null +++ b/workshop-baker/src/finalize/types.rs @@ -0,0 +1 @@ +pub mod compression; diff --git a/workshop-baker/src/types/compression.rs b/workshop-baker/src/finalize/types/compression.rs similarity index 99% rename from workshop-baker/src/types/compression.rs rename to workshop-baker/src/finalize/types/compression.rs index 6d2cdbc..0c9b765 100644 --- a/workshop-baker/src/types/compression.rs +++ b/workshop-baker/src/finalize/types/compression.rs @@ -91,4 +91,4 @@ mod tests { other => panic!("expected Gzip, got {:?}", other), } } -} \ No newline at end of file +} diff --git a/workshop-baker/src/lib.rs b/workshop-baker/src/lib.rs index 6c587a6..7f67be0 100644 --- a/workshop-baker/src/lib.rs +++ b/workshop-baker/src/lib.rs @@ -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}; diff --git a/workshop-baker/src/main.rs b/workshop-baker/src/main.rs index 7aee4a0..ddee145 100644 --- a/workshop-baker/src/main.rs +++ b/workshop-baker/src/main.rs @@ -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::() + .downcast_ref::() .map(|pe| pe.exit_code()) .unwrap_or(1), BareCommands::Bake { .. } => e - .downcast_ref::() + .downcast_ref::() .map(|be| be.exit_code()) .unwrap_or(1), BareCommands::Finalize { .. } => e diff --git a/workshop-baker/src/notify.rs b/workshop-baker/src/notify.rs index ad56c66..9cfaae0 100644 --- a/workshop-baker/src/notify.rs +++ b/workshop-baker/src/notify.rs @@ -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 { + 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, +) -> NotificationTemplate { + let mut merged = method_template.clone(); + if let Some(policy_tmpl) = policy_template { + if let Ok(policy_template) = + serde_yaml::from_value::(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> = HashMap::new(); + for (method_name, method) in group { + let mut method_rules: Vec = Vec::new(); + + if method.policy.is_empty() { + let trigger: Vec = 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 = 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) } diff --git a/workshop-baker/src/notify/error.rs b/workshop-baker/src/notify/error.rs index 02dc745..fbec4e8 100644 --- a/workshop-baker/src/notify/error.rs +++ b/workshop-baker/src/notify/error.rs @@ -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), } diff --git a/workshop-baker/src/notify/types.rs b/workshop-baker/src/notify/types.rs index d6ccfb7..8aba9ae 100644 --- a/workshop-baker/src/notify/types.rs +++ b/workshop-baker/src/notify/types.rs @@ -1,21 +1,25 @@ +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; use crate::finalize::config::Trigger; /// Final runtime configuration for notification, restored and flattened from yaml -pub struct NotificationRule{ +pub struct NotificationRule { pub enabled: bool, - pub trigger: Vec, + pub trigger: Vec, 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>>; -pub type BundledNotificationEvent = Vec; -pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver; +// pub type BundledNotificationEvent = Vec; +pub type NotificationEventReceiver = tokio::sync::mpsc::Receiver; pub type NotificationEventSender = tokio::sync::mpsc::Sender; pub type NotificationPluginMap = HashMap; @@ -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, 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"), + } + } +} diff --git a/workshop-baker/src/prebake.rs b/workshop-baker/src/prebake.rs index 5686ebd..a170d3a 100644 --- a/workshop-baker/src/prebake.rs +++ b/workshop-baker/src/prebake.rs @@ -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?; } diff --git a/workshop-baker/src/prebake/config.rs b/workshop-baker/src/prebake/config.rs index a22a5cd..d4e8434 100644 --- a/workshop-baker/src/prebake/config.rs +++ b/workshop-baker/src/prebake/config.rs @@ -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"; diff --git a/workshop-baker/src/prebake/constant.rs b/workshop-baker/src/prebake/constant.rs new file mode 100644 index 0000000..1de5106 --- /dev/null +++ b/workshop-baker/src/prebake/constant.rs @@ -0,0 +1 @@ +pub const BOOTSTRAP_SCRIPT_PATH: &str = "/bootstrap.sh"; diff --git a/workshop-baker/src/prebake/env/container.rs b/workshop-baker/src/prebake/env/container.rs index 0f20f57..51652e3 100644 --- a/workshop-baker/src/prebake/env/container.rs +++ b/workshop-baker/src/prebake/env/container.rs @@ -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, diff --git a/workshop-baker/src/prebake/error.rs b/workshop-baker/src/prebake/error.rs new file mode 100644 index 0000000..db6b686 --- /dev/null +++ b/workshop-baker/src/prebake/error.rs @@ -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), +} diff --git a/workshop-baker/src/prebake/event.rs b/workshop-baker/src/prebake/event.rs index 3115e01..f4b7da1 100644 --- a/workshop-baker/src/prebake/event.rs +++ b/workshop-baker/src/prebake/event.rs @@ -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, diff --git a/workshop-baker/src/prebake/security.rs b/workshop-baker/src/prebake/security.rs index 0c79a54..5695944 100644 --- a/workshop-baker/src/prebake/security.rs +++ b/workshop-baker/src/prebake/security.rs @@ -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; diff --git a/workshop-baker/src/prebake/stage/bootstrap.rs b/workshop-baker/src/prebake/stage/bootstrap.rs index d4a0d6e..40dca67 100644 --- a/workshop-baker/src/prebake/stage/bootstrap.rs +++ b/workshop-baker/src/prebake/stage/bootstrap.rs @@ -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; diff --git a/workshop-baker/src/prebake/stage/depssystem.rs b/workshop-baker/src/prebake/stage/depssystem.rs index dd3db5d..214553b 100644 --- a/workshop-baker/src/prebake/stage/depssystem.rs +++ b/workshop-baker/src/prebake/stage/depssystem.rs @@ -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; diff --git a/workshop-baker/src/prebake/stage/depsuser.rs b/workshop-baker/src/prebake/stage/depsuser.rs index 5a9ef3b..801c808 100644 --- a/workshop-baker/src/prebake/stage/depsuser.rs +++ b/workshop-baker/src/prebake/stage/depsuser.rs @@ -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)] diff --git a/workshop-baker/src/prebake/stage/environment.rs b/workshop-baker/src/prebake/stage/environment.rs index 05c0e36..afc132f 100644 --- a/workshop-baker/src/prebake/stage/environment.rs +++ b/workshop-baker/src/prebake/stage/environment.rs @@ -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; diff --git a/workshop-baker/src/prebake/stage/hook.rs b/workshop-baker/src/prebake/stage/hook.rs index ce378e2..ef498e8 100644 --- a/workshop-baker/src/prebake/stage/hook.rs +++ b/workshop-baker/src/prebake/stage/hook.rs @@ -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. diff --git a/workshop-baker/src/prebake/types.rs b/workshop-baker/src/prebake/types.rs new file mode 100644 index 0000000..b7635ac --- /dev/null +++ b/workshop-baker/src/prebake/types.rs @@ -0,0 +1,4 @@ +pub mod architecture; +pub mod builderconfig; +pub mod cache; +pub mod memsize; diff --git a/workshop-baker/src/types/architecture.rs b/workshop-baker/src/prebake/types/architecture.rs similarity index 72% rename from workshop-baker/src/types/architecture.rs rename to workshop-baker/src/prebake/types/architecture.rs index de435b3..a46e014 100644 --- a/workshop-baker/src/types/architecture.rs +++ b/workshop-baker/src/prebake/types/architecture.rs @@ -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::().unwrap(), Architecture::NoArch); - assert_eq!("x86_64".parse::().unwrap(), Architecture::Amd64); - assert_eq!("arm64".parse::().unwrap(), Architecture::Aarch64); - assert_eq!("riscv64".parse::().unwrap(), Architecture::Riscv64); - assert_eq!("loongarch64".parse::().unwrap(), Architecture::Loong64); + assert_eq!( + "noarch".parse::().unwrap(), + Architecture::NoArch + ); + assert_eq!( + "x86_64".parse::().unwrap(), + Architecture::Amd64 + ); + assert_eq!( + "arm64".parse::().unwrap(), + Architecture::Aarch64 + ); + assert_eq!( + "riscv64".parse::().unwrap(), + Architecture::Riscv64 + ); + assert_eq!( + "loongarch64".parse::().unwrap(), + Architecture::Loong64 + ); } #[test] fn test_from_str_valid_aliases() { - assert_eq!("amd64".parse::().unwrap(), Architecture::Amd64); - assert_eq!("aarch64".parse::().unwrap(), Architecture::Aarch64); - assert_eq!("loong64".parse::().unwrap(), Architecture::Loong64); + assert_eq!( + "amd64".parse::().unwrap(), + Architecture::Amd64 + ); + assert_eq!( + "aarch64".parse::().unwrap(), + Architecture::Aarch64 + ); + assert_eq!( + "loong64".parse::().unwrap(), + Architecture::Loong64 + ); } #[test] fn test_from_str_case_insensitive() { - assert_eq!("X86_64".parse::().unwrap(), Architecture::Amd64); - assert_eq!("ARM64".parse::().unwrap(), Architecture::Aarch64); - assert_eq!("RISCV64".parse::().unwrap(), Architecture::Riscv64); - assert_eq!("NoArch".parse::().unwrap(), Architecture::NoArch); - assert_eq!("LoongArch64".parse::().unwrap(), Architecture::Loong64); + assert_eq!( + "X86_64".parse::().unwrap(), + Architecture::Amd64 + ); + assert_eq!( + "ARM64".parse::().unwrap(), + Architecture::Aarch64 + ); + assert_eq!( + "RISCV64".parse::().unwrap(), + Architecture::Riscv64 + ); + assert_eq!( + "NoArch".parse::().unwrap(), + Architecture::NoArch + ); + assert_eq!( + "LoongArch64".parse::().unwrap(), + Architecture::Loong64 + ); } #[test] @@ -184,4 +229,4 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().contains("Unknown architecture")); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/types/builderconfig.rs b/workshop-baker/src/prebake/types/builderconfig.rs similarity index 96% rename from workshop-baker/src/types/builderconfig.rs rename to workshop-baker/src/prebake/types/builderconfig.rs index a356d6a..8843e61 100644 --- a/workshop-baker/src/types/builderconfig.rs +++ b/workshop-baker/src/prebake/types/builderconfig.rs @@ -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(); @@ -122,4 +125,4 @@ mod tests { let config: BuilderConfig = serde_yaml::from_str(yaml).unwrap(); assert!(matches!(config, BuilderConfig::Docker(_))); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/types/cache.rs b/workshop-baker/src/prebake/types/cache.rs similarity index 99% rename from workshop-baker/src/types/cache.rs rename to workshop-baker/src/prebake/types/cache.rs index 558256f..a4991f7 100644 --- a/workshop-baker/src/types/cache.rs +++ b/workshop-baker/src/prebake/types/cache.rs @@ -231,4 +231,4 @@ mod tests { let parsed: CleanupPolicy = serde_json::from_str(&json).unwrap(); assert_eq!(parsed, val); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/types/memsize.rs b/workshop-baker/src/prebake/types/memsize.rs similarity index 100% rename from workshop-baker/src/types/memsize.rs rename to workshop-baker/src/prebake/types/memsize.rs diff --git a/workshop-baker/src/types.rs b/workshop-baker/src/types.rs index bfb0417..5ecc84f 100644 --- a/workshop-baker/src/types.rs +++ b/workshop-baker/src/types.rs @@ -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; +pub mod buildstatus; \ No newline at end of file diff --git a/workshop-baker/src/types/buildstatus.rs b/workshop-baker/src/types/buildstatus.rs index 740e44b..527bd1d 100644 --- a/workshop-baker/src/types/buildstatus.rs +++ b/workshop-baker/src/types/buildstatus.rs @@ -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>(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>(status_dir: P) -> io::Result { - 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 = 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::, _>>()?; - - 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); - } -} \ No newline at end of file +} diff --git a/workshop-baker/tests/bake_pipeline_integration.rs b/workshop-baker/tests/bake_pipeline_integration.rs index 34fb559..b51f9d7 100644 --- a/workshop-baker/tests/bake_pipeline_integration.rs +++ b/workshop-baker/tests/bake_pipeline_integration.rs @@ -81,4 +81,4 @@ deploy() { assert_eq!(parsed.pipeline_functions.len(), 1); assert_eq!(parsed.pipeline_functions[0].name, "deploy"); assert!(parsed.pipeline_functions[0].body.contains("deploying")); -} \ No newline at end of file +} diff --git a/workshop-baker/tests/prebake_config_integration.rs b/workshop-baker/tests/prebake_config_integration.rs index 3b10cbe..48b9f4b 100644 --- a/workshop-baker/tests/prebake_config_integration.rs +++ b/workshop-baker/tests/prebake_config_integration.rs @@ -76,4 +76,4 @@ environment: "#; let config = parse(yaml).unwrap(); assert!(config.validate().is_err()); -} \ No newline at end of file +} diff --git a/workshop-baker/tests/types_config_integration.rs b/workshop-baker/tests/types_config_integration.rs index bd2cfcc..0ce94ce 100644 --- a/workshop-baker/tests/types_config_integration.rs +++ b/workshop-baker/tests/types_config_integration.rs @@ -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] @@ -86,4 +86,4 @@ fn test_custom_command_default_timeout() { let cmd: CustomCommand = serde_yaml::from_str(yaml).unwrap(); assert_eq!(cmd.name, "(unnamed)"); assert_eq!(cmd.timeout_ms, 300_000); -} \ No newline at end of file +} diff --git a/workshop-engine/Cargo.lock b/workshop-engine/Cargo.lock new file mode 100644 index 0000000..c4c70db --- /dev/null +++ b/workshop-engine/Cargo.lock @@ -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" diff --git a/workshop-engine/Cargo.toml b/workshop-engine/Cargo.toml new file mode 100644 index 0000000..d91d3f5 --- /dev/null +++ b/workshop-engine/Cargo.toml @@ -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" diff --git a/workshop-baker/src/engine/cgroups.rs b/workshop-engine/src/cgroups.rs similarity index 100% rename from workshop-baker/src/engine/cgroups.rs rename to workshop-engine/src/cgroups.rs diff --git a/workshop-baker/src/types/command.rs b/workshop-engine/src/command.rs similarity index 98% rename from workshop-baker/src/types/command.rs rename to workshop-engine/src/command.rs index 67c8e8b..1a7c1b7 100644 --- a/workshop-baker/src/types/command.rs +++ b/workshop-engine/src/command.rs @@ -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; @@ -92,4 +92,4 @@ mod tests { assert_eq!(cmd.name, "(unnamed)"); assert_eq!(cmd.timeout_ms, 300_000); } -} \ No newline at end of file +} diff --git a/workshop-engine/src/error.rs b/workshop-engine/src/error.rs new file mode 100644 index 0000000..790bfb2 --- /dev/null +++ b/workshop-engine/src/error.rs @@ -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 + } +} diff --git a/workshop-baker/src/engine/executor.rs b/workshop-engine/src/executor.rs similarity index 99% rename from workshop-baker/src/engine/executor.rs rename to workshop-engine/src/executor.rs index a8a5907..795ad61 100644 --- a/workshop-baker/src/engine/executor.rs +++ b/workshop-engine/src/executor.rs @@ -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() } diff --git a/workshop-engine/src/lib.rs b/workshop-engine/src/lib.rs new file mode 100644 index 0000000..97b00c5 --- /dev/null +++ b/workshop-engine/src/lib.rs @@ -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; diff --git a/workshop-baker/src/engine/pm.rs b/workshop-engine/src/pm.rs similarity index 100% rename from workshop-baker/src/engine/pm.rs rename to workshop-engine/src/pm.rs diff --git a/workshop-baker/src/engine/pm/apt.rs b/workshop-engine/src/pm/apt.rs similarity index 98% rename from workshop-baker/src/engine/pm/apt.rs rename to workshop-engine/src/pm/apt.rs index bb93533..a84a9d9 100644 --- a/workshop-baker/src/engine/pm/apt.rs +++ b/workshop-engine/src/pm/apt.rs @@ -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; diff --git a/workshop-baker/src/engine/pm/pacman.rs b/workshop-engine/src/pm/pacman.rs similarity index 99% rename from workshop-baker/src/engine/pm/pacman.rs rename to workshop-engine/src/pm/pacman.rs index e723566..9150f2a 100644 --- a/workshop-baker/src/engine/pm/pacman.rs +++ b/workshop-engine/src/pm/pacman.rs @@ -1,4 +1,4 @@ -use super::PackageManager; +use crate::pm::PackageManager; use os_info::{Info, Type}; use serde::Deserialize; use serde_yaml::Value; diff --git a/workshop-baker/src/types/repology.rs b/workshop-engine/src/repology.rs similarity index 80% rename from workshop-baker/src/types/repology.rs rename to workshop-engine/src/repology.rs index a958348..c51cd76 100644 --- a/workshop-baker/src/types/repology.rs +++ b/workshop-engine/src/repology.rs @@ -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>, 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::*; @@ -148,4 +194,4 @@ mod tests { assert_eq!(deserialized, ep); } } -} \ No newline at end of file +} diff --git a/workshop-baker/src/engine/repology/local.rs b/workshop-engine/src/repology/local.rs similarity index 100% rename from workshop-baker/src/engine/repology/local.rs rename to workshop-engine/src/repology/local.rs diff --git a/workshop-baker/src/types/time.rs b/workshop-engine/src/time.rs similarity index 99% rename from workshop-baker/src/types/time.rs rename to workshop-engine/src/time.rs index c69cf33..2ab3c9e 100644 --- a/workshop-baker/src/types/time.rs +++ b/workshop-engine/src/time.rs @@ -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); @@ -308,4 +308,4 @@ mod tests { let t: DirectOptStr = serde_json::from_str(json).unwrap(); assert_eq!(t.duration_ms, Some("2m".to_string())); } -} \ No newline at end of file +} diff --git a/workshop-baker/src/engine/types.rs b/workshop-engine/src/types.rs similarity index 98% rename from workshop-baker/src/engine/types.rs rename to workshop-engine/src/types.rs index 9923a95..9a0e468 100644 --- a/workshop-baker/src/engine/types.rs +++ b/workshop-engine/src/types.rs @@ -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)] diff --git a/workshop-baker/src/engine/upm.rs b/workshop-engine/src/upm.rs similarity index 95% rename from workshop-baker/src/engine/upm.rs rename to workshop-engine/src/upm.rs index b97d523..1b5b764 100644 --- a/workshop-baker/src/engine/upm.rs +++ b/workshop-engine/src/upm.rs @@ -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 { diff --git a/workshop-baker/src/engine/upm/custom.rs b/workshop-engine/src/upm/custom.rs similarity index 88% rename from workshop-baker/src/engine/upm/custom.rs rename to workshop-engine/src/upm/custom.rs index 5dbee90..e0f9453 100644 --- a/workshop-baker/src/engine/upm/custom.rs +++ b/workshop-engine/src/upm/custom.rs @@ -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,