Skip to content

Commit 77907d0

Browse files
committed
👷 instlib: emit step progress from pacman output
1 parent 3d91fe8 commit 77907d0

4 files changed

Lines changed: 217 additions & 38 deletions

File tree

‎installer-lib/include/cachyos/orchestrator.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
#include "cachyos/types.hpp"
55

6+
#include <optional> // for optional
67
#include <string_view> // for string_view
78

89
namespace cachyos::installer {
910

11+
/// Parses a pacman-style "( N/M)" progress prefix from a log line.
12+
[[nodiscard]] auto parse_pacman_progress(std::string_view line) noexcept -> std::optional<double>;
13+
1014
/// Runs the full install sequence from a populated context.
1115
///
1216
/// Mutates @p ctx as the install progresses: partition_schema and swap_device

‎installer-lib/src/orchestrator.cpp‎

Lines changed: 142 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
#include "cachyos/validation.hpp"
88

99
// import gucc
10+
#include "gucc/string_utils.hpp"
1011
#include "gucc/subprocess.hpp"
1112

13+
#include <array> // for array
14+
#include <cstdint> // for uint8_t, uint32_t
1215
#include <filesystem> // for copy_file, exists
16+
#include <optional> // for optional
1317
#include <string> // for string
1418
#include <string_view> // for string_view
1519
#include <utility> // for move
@@ -26,23 +30,80 @@ namespace {
2630
// NOLINTNEXTLINE
2731
using namespace cachyos::installer;
2832

29-
constexpr int kTotalSteps = 12;
33+
enum class Step : std::uint8_t {
34+
Umount,
35+
Partition,
36+
Fstab,
37+
SystemSettings,
38+
Users,
39+
Base,
40+
Desktop,
41+
Bootloader,
42+
DetectCrypto,
43+
EnableServices,
44+
FinalValidation,
45+
Cleanup,
46+
Count,
47+
};
48+
49+
constexpr std::int32_t kTotalSteps = static_cast<std::int32_t>(Step::Count);
50+
51+
constexpr std::array<std::string_view, kTotalSteps> kStepMessages = {
52+
"Unmounting existing partitions..."sv,
53+
"Partitioning and mounting..."sv,
54+
"Generating fstab..."sv,
55+
"Configuring system settings..."sv,
56+
"Creating user accounts..."sv,
57+
"Installing base system (this may take a while)..."sv,
58+
"Installing desktop environment..."sv,
59+
"Installing bootloader..."sv,
60+
"Detecting encryption state..."sv,
61+
"Enabling system services..."sv,
62+
"Running final validation..."sv,
63+
"Cleaning up..."sv,
64+
};
65+
66+
constexpr auto step_index(Step s) noexcept {
67+
return static_cast<std::int32_t>(s);
68+
}
69+
70+
constexpr auto step_message(Step s) noexcept -> std::string_view {
71+
return kStepMessages[static_cast<std::size_t>(s)];
72+
}
3073

3174
auto emit_progress(const ExecutionCallbacks& cb,
3275
ProgressEventType type,
33-
int step,
76+
std::int32_t step,
3477
std::string_view message) noexcept -> void {
3578
if (!cb.on_progress) {
3679
return;
3780
}
38-
const double fraction = static_cast<double>(step) / static_cast<double>(kTotalSteps);
81+
const auto fraction = static_cast<double>(step) / static_cast<double>(kTotalSteps);
3982
cb.on_progress(ProgressEvent{
4083
.type = type,
4184
.message = std::string{message},
4285
.fraction = fraction,
4386
});
4487
}
4588

89+
void emit_step_running(const ExecutionCallbacks& cb, Step s) noexcept {
90+
emit_progress(cb, ProgressEventType::Running, step_index(s), step_message(s));
91+
}
92+
93+
/// Emit a Failed event and return a ValidationResult with the formatted error.
94+
auto fail_step(const ExecutionCallbacks& cb,
95+
Step s,
96+
std::string_view label,
97+
std::string_view error,
98+
std::vector<std::string> prior_warnings) noexcept -> ValidationResult {
99+
emit_progress(cb, ProgressEventType::Failed, step_index(s), label);
100+
return ValidationResult{
101+
.success = false,
102+
.errors = {fmt::format("{}: {}", label, error)},
103+
.warnings = std::move(prior_warnings),
104+
};
105+
}
106+
46107
auto mount_selections_from_auto(const std::vector<gucc::fs::Partition>& partitions) noexcept -> MountSelections {
47108
MountSelections mounts{};
48109
for (const auto& part : partitions) {
@@ -68,18 +129,67 @@ auto mount_selections_from_auto(const std::vector<gucc::fs::Partition>& partitio
68129
return mounts;
69130
}
70131

71-
auto make_failure(std::string message, std::vector<std::string> prior_warnings) noexcept -> ValidationResult {
72-
return ValidationResult{
73-
.success = false,
74-
.errors = {std::move(message)},
75-
.warnings = std::move(prior_warnings),
132+
/// Builds a log-line callback for a step that forwards lines to the user's
133+
/// sink and, when a pacman progress line is recognised, emits an intra-step
134+
/// Running event scaled into this step's slice of the overall progress bar.
135+
auto step_log_callback(const ExecutionCallbacks& cb, Step s) noexcept
136+
-> gucc::utils::SubProcess::LogLineCallback {
137+
if (!cb.on_log_line && !cb.on_progress) {
138+
return {};
139+
}
140+
const auto idx = step_index(s);
141+
auto msg = std::string{step_message(s)};
142+
return [&cb, idx, msg = std::move(msg)](std::string_view line) {
143+
if (cb.on_log_line) {
144+
cb.on_log_line(line);
145+
}
146+
if (!cb.on_progress) {
147+
return;
148+
}
149+
const auto frac = parse_pacman_progress(line);
150+
if (!frac) {
151+
return;
152+
}
153+
constexpr auto total = static_cast<double>(kTotalSteps);
154+
const double base = static_cast<double>(idx) / total;
155+
const double step_width = 1.0 / total;
156+
cb.on_progress(ProgressEvent{
157+
.type = ProgressEventType::Running,
158+
.message = msg,
159+
.fraction = base + (*frac * step_width),
160+
});
76161
};
77162
}
78163

79164
} // namespace
80165

81166
namespace cachyos::installer {
82167

168+
auto parse_pacman_progress(std::string_view line) noexcept -> std::optional<double> {
169+
const auto open = line.find('(');
170+
if (open == std::string_view::npos) {
171+
return std::nullopt;
172+
}
173+
const auto slash = line.find('/', open + 1);
174+
if (slash == std::string_view::npos) {
175+
return std::nullopt;
176+
}
177+
const auto close = line.find(')', slash + 1);
178+
if (close == std::string_view::npos) {
179+
return std::nullopt;
180+
}
181+
182+
const auto num_str = gucc::utils::trim(line.substr(open + 1, slash - open - 1));
183+
const auto denom_str = gucc::utils::trim(line.substr(slash + 1, close - slash - 1));
184+
185+
const auto num = gucc::utils::parse_uint<std::uint32_t>(num_str);
186+
const auto denom = gucc::utils::parse_uint<std::uint32_t>(denom_str);
187+
if (!num || !denom || *denom == 0 || *num > *denom) {
188+
return std::nullopt;
189+
}
190+
return static_cast<double>(*num) / static_cast<double>(*denom);
191+
}
192+
83193
auto run(InstallContext& ctx,
84194
const SystemSettings& sys,
85195
const UserSettings& user,
@@ -90,100 +200,95 @@ auto run(InstallContext& ctx,
90200
std::vector<std::string> warnings;
91201

92202
spdlog::info("Install orchestrator starting...");
93-
emit_progress(callbacks, Started, 0, "Starting installation...");
203+
emit_progress(callbacks, Started, 0, "Starting installation..."sv);
94204

95205
// Step 1: Unmount any existing partitions on the target.
96-
emit_progress(callbacks, Running, 0, "Unmounting existing partitions...");
206+
emit_step_running(callbacks, Step::Umount);
97207
if (auto res = umount_partitions(mountpoint, ctx.zfs_zpool_names, ctx.swap_device); !res) {
98208
spdlog::warn("umount_partitions (pre-install): {}", res.error());
99209
warnings.emplace_back(fmt::format("Pre-install unmount: {}", res.error()));
100210
}
101211

102212
// Step 2: Auto-partition and mount.
103-
emit_progress(callbacks, Running, 1, "Partitioning and mounting...");
213+
emit_step_running(callbacks, Step::Partition);
104214
{
105215
const auto& bios_mode = ctx.system_mode == InstallContext::SystemMode::UEFI ? "UEFI"sv : "BIOS"sv;
106-
auto partitions = auto_partition(ctx.device, bios_mode, ctx.bootloader, callbacks);
216+
auto partitions = auto_partition(ctx.device, bios_mode, ctx.bootloader, callbacks);
107217
if (!partitions) {
108-
emit_progress(callbacks, Failed, 1, "Auto-partition failed");
109-
return make_failure(fmt::format("Auto-partition failed: {}", partitions.error()), std::move(warnings));
218+
return fail_step(callbacks, Step::Partition, "Auto-partition failed"sv, partitions.error(), std::move(warnings));
110219
}
111220

112221
const auto mounts = mount_selections_from_auto(*partitions);
113222
auto mount_res = apply_mount_selections(mounts, mountpoint);
114223
if (!mount_res) {
115-
emit_progress(callbacks, Failed, 1, "Mount failed");
116-
return make_failure(fmt::format("Mount failed: {}", mount_res.error()), std::move(warnings));
224+
return fail_step(callbacks, Step::Partition, "Mount failed"sv, mount_res.error(), std::move(warnings));
117225
}
118226

119227
ctx.partition_schema = std::move(mount_res->partitions);
120228
ctx.swap_device = std::move(mount_res->swap_device);
121229
}
122230

123231
// Step 3: Generate fstab.
124-
emit_progress(callbacks, Running, 2, "Generating fstab...");
232+
emit_step_running(callbacks, Step::Fstab);
125233
if (auto res = generate_fstab(mountpoint); !res) {
126-
emit_progress(callbacks, Failed, 2, "fstab generation failed");
127-
return make_failure(fmt::format("fstab generation failed: {}", res.error()), std::move(warnings));
234+
return fail_step(callbacks, Step::Fstab, "fstab generation failed"sv, res.error(), std::move(warnings));
128235
}
129236

130237
// Step 4: Apply system settings (hostname, locale, keymap, timezone, hw_clock).
131-
emit_progress(callbacks, Running, 3, "Configuring system settings...");
238+
emit_step_running(callbacks, Step::SystemSettings);
132239
if (auto res = apply_system_settings(sys, mountpoint); !res) {
133-
emit_progress(callbacks, Failed, 3, "System settings failed");
134-
return make_failure(fmt::format("System settings failed: {}", res.error()), std::move(warnings));
240+
return fail_step(callbacks, Step::SystemSettings, "System settings failed"sv, res.error(), std::move(warnings));
135241
}
136242

137243
// Step 5: Root password + user account.
138-
emit_progress(callbacks, Running, 4, "Creating user accounts...");
244+
emit_step_running(callbacks, Step::Users);
139245
if (auto res = set_root_password(root_password, mountpoint); !res) {
140246
spdlog::error("set_root_password: {}", res.error());
141247
warnings.emplace_back(fmt::format("set_root_password: {}", res.error()));
142248
}
143249
{
144250
gucc::utils::SubProcess child;
145-
child.set_log_line_callback(callbacks.on_log_line);
251+
child.set_log_line_callback(step_log_callback(callbacks, Step::Users));
146252
if (auto res = create_user(user, mountpoint, ctx.hostcache, child); !res) {
147253
spdlog::error("create_user: {}", res.error());
148254
warnings.emplace_back(fmt::format("create_user: {}", res.error()));
149255
}
150256
}
151257

152258
// Step 6: Base system.
153-
emit_progress(callbacks, Running, 5, "Installing base system (this may take a while)...");
259+
emit_step_running(callbacks, Step::Base);
154260
{
155261
gucc::utils::SubProcess child;
156-
child.set_log_line_callback(callbacks.on_log_line);
262+
child.set_log_line_callback(step_log_callback(callbacks, Step::Base));
157263
if (auto res = install_base(ctx, child); !res) {
158-
emit_progress(callbacks, Failed, 5, "Base install failed");
159-
return make_failure(fmt::format("Base install failed: {}", res.error()), std::move(warnings));
264+
return fail_step(callbacks, Step::Base, "Base install failed"sv, res.error(), std::move(warnings));
160265
}
161266
}
162267

163268
// Step 7: Desktop (skipped in server mode).
164-
emit_progress(callbacks, Running, 6, "Installing desktop environment...");
269+
emit_step_running(callbacks, Step::Desktop);
165270
if (!ctx.server_mode && !ctx.desktop.empty()) {
166271
gucc::utils::SubProcess child;
167-
child.set_log_line_callback(callbacks.on_log_line);
272+
child.set_log_line_callback(step_log_callback(callbacks, Step::Desktop));
168273
if (auto res = install_desktop(ctx.desktop, ctx, child); !res) {
169274
spdlog::error("install_desktop: {}", res.error());
170275
warnings.emplace_back(fmt::format("install_desktop: {}", res.error()));
171276
}
172277
}
173278

174279
// Step 8: Bootloader.
175-
emit_progress(callbacks, Running, 7, "Installing bootloader...");
280+
emit_step_running(callbacks, Step::Bootloader);
176281
{
177282
gucc::utils::SubProcess child;
178-
child.set_log_line_callback(callbacks.on_log_line);
283+
child.set_log_line_callback(step_log_callback(callbacks, Step::Bootloader));
179284
if (auto res = install_bootloader(ctx, child); !res) {
180285
spdlog::error("install_bootloader: {}", res.error());
181286
warnings.emplace_back(fmt::format("install_bootloader: {}", res.error()));
182287
}
183288
}
184289

185290
// Step 9: Detect post-install crypto state and stash it on the context for kernel-params use.
186-
emit_progress(callbacks, Running, 8, "Detecting encryption state...");
291+
emit_step_running(callbacks, Step::DetectCrypto);
187292
if (auto res = detect_crypto_root(mountpoint); res) {
188293
ctx.crypto.is_luks = res->is_luks;
189294
ctx.crypto.is_lvm = res->is_lvm;
@@ -195,14 +300,14 @@ auto run(InstallContext& ctx,
195300
}
196301

197302
// Step 10: Enable systemd services.
198-
emit_progress(callbacks, Running, 9, "Enabling system services...");
303+
emit_step_running(callbacks, Step::EnableServices);
199304
if (auto res = enable_services(ctx); !res) {
200305
spdlog::error("enable_services: {}", res.error());
201306
warnings.emplace_back(fmt::format("enable_services: {}", res.error()));
202307
}
203308

204309
// Step 11: Final validation.
205-
emit_progress(callbacks, Running, 10, "Running final validation...");
310+
emit_step_running(callbacks, Step::FinalValidation);
206311
{
207312
auto check = final_check(ctx);
208313
for (auto& err : check.errors) {
@@ -216,7 +321,7 @@ auto run(InstallContext& ctx,
216321
}
217322

218323
// Step 12: Copy install log into target and unmount.
219-
emit_progress(callbacks, Running, 11, "Cleaning up...");
324+
emit_step_running(callbacks, Step::Cleanup);
220325
{
221326
constexpr auto kLogSource = "/tmp/cachyos-install.log"sv;
222327
if (fs::exists(kLogSource)) {
@@ -234,7 +339,7 @@ auto run(InstallContext& ctx,
234339
warnings.emplace_back(fmt::format("Final umount: {}", res.error()));
235340
}
236341

237-
emit_progress(callbacks, Completed, kTotalSteps, "Installation complete!");
342+
emit_progress(callbacks, Completed, kTotalSteps, "Installation complete!"sv);
238343
spdlog::info("Install orchestrator finished.");
239344

240345
return ValidationResult{

‎tests/CMakeLists.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ foreach(file ${files})
3535
$<$<CXX_COMPILER_ID:GNU>:-Wno-deprecated-declarations>
3636
)
3737
target_include_directories(${testcase} PRIVATE ${CMAKE_BINARY_DIR}/include ${CMAKE_SOURCE_DIR}/include ${CMAKE_CURRENT_DIR} ${CMAKE_SOURCE_DIR}/gucc/tests)
38-
target_link_libraries(${testcase} PRIVATE project_warnings project_options test_libreq spdlog::spdlog fmt::fmt ftxui::component ctre::ctre tomlplusplus::tomlplusplus gucc::gucc doctest::doctest)
38+
target_link_libraries(${testcase} PRIVATE project_warnings project_options test_libreq spdlog::spdlog fmt::fmt ftxui::component ctre::ctre tomlplusplus::tomlplusplus gucc::gucc cachyos::installer-lib doctest::doctest)
3939
if(NOT ENABLE_DEVENV)
4040
target_link_libraries(${testcase} PRIVATE cpr::cpr)
4141
endif()

0 commit comments

Comments
 (0)