Skip to main content

light_fw_cli_core/
build.rs

1//! Логика команды `light build`: сборка приложения через `cargo build --release`.
2//!
3//! Разделение на чистые функции ([`build_args`], [`locate_binary`],
4//! [`format_size`]) и запуск процесса ([`run_build`]) позволяет тестировать
5//! формирование аргументов и поиск бинарного файла без реального запуска cargo.
6
7use std::path::{Path, PathBuf};
8
9use crate::error::{Error, Result};
10use crate::process::CommandRunner;
11
12/// Опции сборки.
13#[derive(Debug, Clone, Default)]
14pub struct BuildOptions {
15    /// Релизная сборка (по умолчанию).
16    pub release: bool,
17    /// Фичи для включения (`--features a,b`).
18    pub features: Vec<String>,
19    /// Не включать дефолтные фичи (`--no-default-features`).
20    pub no_default_features: bool,
21    /// Дополнительные аргументы, передаваемые в `cargo build` как есть.
22    pub extra_cargo_args: Vec<String>,
23}
24
25impl BuildOptions {
26    /// Опции по умолчанию: релизная сборка без фич.
27    pub fn default_release() -> Self {
28        Self {
29            release: true,
30            ..Self::default()
31        }
32    }
33}
34
35/// Формирует вектор аргументов для `cargo build`.
36pub fn build_args(opts: &BuildOptions) -> Vec<String> {
37    let mut args = vec!["build".to_string()];
38    if opts.release {
39        args.push("--release".to_string());
40    }
41    if opts.no_default_features {
42        args.push("--no-default-features".to_string());
43    }
44    if !opts.features.is_empty() {
45        args.push("--features".to_string());
46        args.push(opts.features.join(","));
47    }
48    args.extend(opts.extra_cargo_args.iter().cloned());
49    args
50}
51
52/// Запускает `cargo build` и проверяет код возврата.
53pub fn run_build(
54    runner: &dyn CommandRunner,
55    cwd: &Path,
56    opts: &BuildOptions,
57) -> Result<crate::process::CommandOutput> {
58    let label = if opts.release {
59        "Сборка (cargo build --release)"
60    } else {
61        "Сборка (cargo build)"
62    };
63    let output = runner.run_with_progress("cargo", &build_args(opts), Some(cwd), label)?;
64    if !output.success() {
65        return Err(Error::CommandFailed {
66            program: "cargo build".to_string(),
67            code: output.status,
68            stderr: output.stderr.trim().to_string(),
69        });
70    }
71    Ok(output)
72}
73
74/// Определяет путь к собранному бинарнику.
75///
76/// Ищет `<target_dir>/<release|debug>/<bin_name>` с учётом расширения `.exe`
77/// на Windows.
78pub fn locate_binary(target_dir: &Path, bin_name: &str, release: bool) -> Result<PathBuf> {
79    let profile = if release { "release" } else { "debug" };
80    let base = target_dir.join(profile).join(bin_name);
81    let candidates = [
82        format!("{}.exe", base.display()),
83        base.to_string_lossy().into_owned(),
84    ];
85    for candidate in &candidates {
86        let p = PathBuf::from(candidate);
87        if p.exists() {
88            return Ok(p);
89        }
90    }
91    // Возвращаем ожидаемый путь, даже если файл не найден — вызывающий код
92    // может отобразить его пользователю.
93    Ok(candidates[0].clone().into())
94}
95
96/// Форматирует размер файла в человекочитаемом виде.
97pub fn format_size(bytes: u64) -> String {
98    if bytes < 1024 {
99        format!("{bytes} B")
100    } else if bytes < 1024 * 1024 {
101        format!("{:.1} KiB", bytes as f64 / 1024.0)
102    } else if bytes < 1024 * 1024 * 1024 {
103        format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
104    } else {
105        format!("{:.1} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn release_build_args() {
115        let opts = BuildOptions::default_release();
116        assert_eq!(build_args(&opts), vec!["build", "--release"]);
117    }
118
119    #[test]
120    fn debug_build_args() {
121        let opts = BuildOptions::default();
122        assert_eq!(build_args(&opts), vec!["build"]);
123    }
124
125    #[test]
126    fn build_args_with_features() {
127        let mut opts = BuildOptions::default_release();
128        opts.features = vec!["a".to_string(), "b".to_string()];
129        assert_eq!(
130            build_args(&opts),
131            vec!["build", "--release", "--features", "a,b"]
132        );
133    }
134
135    #[test]
136    fn build_args_no_default_features() {
137        let mut opts = BuildOptions::default_release();
138        opts.no_default_features = true;
139        assert_eq!(
140            build_args(&opts),
141            vec!["build", "--release", "--no-default-features"]
142        );
143    }
144
145    #[test]
146    fn build_args_with_extra() {
147        let mut opts = BuildOptions::default_release();
148        opts.extra_cargo_args = vec!["--jobs".to_string(), "4".to_string()];
149        assert_eq!(build_args(&opts), vec!["build", "--release", "--jobs", "4"]);
150    }
151
152    #[test]
153    fn format_size_bytes() {
154        assert_eq!(format_size(512), "512 B");
155        assert_eq!(format_size(1023), "1023 B");
156    }
157
158    #[test]
159    fn format_size_kib() {
160        assert_eq!(format_size(1024), "1.0 KiB");
161        assert_eq!(format_size(1536), "1.5 KiB");
162    }
163
164    #[test]
165    fn format_size_mib() {
166        assert_eq!(format_size(1024 * 1024), "1.0 MiB");
167        assert_eq!(format_size(2 * 1024 * 1024 + 512 * 1024), "2.5 MiB");
168    }
169
170    #[test]
171    fn format_size_gib() {
172        assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GiB");
173    }
174
175    #[test]
176    fn locate_binary_existent() {
177        let dir = std::env::temp_dir().join(format!("light-build-test-{}", std::process::id()));
178        let release_dir = dir.join("release");
179        if let Err(e) = std::fs::create_dir_all(&release_dir) {
180            eprintln!("skip: cannot create temp dir: {e}");
181            return;
182        }
183        let bin = release_dir.join("myapp.exe");
184        std::fs::write(&bin, b"fake").unwrap();
185        let found = locate_binary(&dir, "myapp", true).unwrap();
186        assert_eq!(found, bin);
187        let _ = std::fs::remove_dir_all(&dir);
188    }
189
190    #[test]
191    fn locate_binary_nonexistent_returns_expected_path() {
192        let dir = std::env::temp_dir().join("light-build-missing");
193        let expected = dir.join("release").join("nothere.exe");
194        let found = locate_binary(&dir, "nothere", true).unwrap();
195        assert_eq!(found, expected);
196    }
197}