cargo_wrap/
lib.rs

1use std::{env, fs, io};
2use std::fs::OpenOptions;
3use std::io::{Error, ErrorKind, Write};
4use std::path::PathBuf;
5use std::process::Command;
6use toml::Value;
7
8
9/// Holds configuration settings for a Rust project build.
10///
11/// This struct encapsulates settings like build target, output paths, enabled features, and
12/// release/debug modes.
13///
14/// # Fields
15///
16/// * `compilation_target` - Optional string specifying a custom compilation target.
17/// * `features` - Optional list of features to enable during the build.
18/// * `output_path` - Optional path to store compiled artifacts.
19/// * `release` - Whether to compile in release mode (`true`) or debug mode (`false`).
20/// * `is_lib` - If `true`, builds the project as a library (`--lib`), otherwise builds as a binary (`--bin`).
21/// * `no_default_features` - If `true`, disables default features (`--no-default-features`).
22/// * `project_path` - The root directory of the Rust project.
23/// * `cargo_toml_path` - Path to the project's `Cargo.toml`.
24/// * `target` - Optional specific binary/library to build.
25#[derive(Default, Debug)]
26pub struct ProjectSettings {
27    compilation_target: Option<String>,
28    features: Option<Vec<String>>,
29    output_path: Option<PathBuf>,
30    release: bool,
31    is_lib: bool,
32    no_default_features: bool,
33    project_path: PathBuf,
34    cargo_toml_path: PathBuf,
35    target: Option<String>
36}
37
38impl ProjectSettings {
39
40    /// Creates a new `ProjectSettings` instance for managing build configurations.
41    ///
42    /// # Arguments
43    ///
44    /// * `project_path` - The root directory of the Rust project.
45    /// * `output_path` - Optional path where the build output should be stored.
46    /// * `target` - Optional target triple (e.g., "x86_64-unknown-linux-gnu").
47    /// * `is_lib` - If `true`, builds the project as a library (`--lib`). If `false`, builds as a binary (`--bin`).
48    ///
49    /// # Returns
50    ///
51    /// A `ProjectSettings` instance with default values.
52    ///
53    /// # Example
54    /// ```rust
55    /// use cargo_wrap::ProjectSettings;
56    /// let settings = ProjectSettings::new("/path/to/project", None, None, false);
57    /// ```
58    pub fn new(project_path: impl Into<PathBuf>, output_path: Option<impl Into<PathBuf>>, target: Option<String>,
59               is_lib: bool) -> Self {
60        let project_path = project_path.into();
61        let cargo_toml = project_path.clone().join("Cargo.toml");
62        Self {
63            project_path,
64            release: false,
65            output_path: output_path.map(Into::into),
66            cargo_toml_path: cargo_toml,
67            is_lib,
68            target,
69            ..Default::default()
70        }
71    }
72
73    /// Retrieves a list of available features from `Cargo.toml`.
74    ///
75    /// # Returns
76    ///
77    /// * `Ok(Vec<String>)` - A list of feature names if parsing succeeds.
78    /// * `Err(io::Error)` - If `Cargo.toml` is missing or cannot be parsed.
79    ///
80    /// # Errors
81    ///
82    /// This function will return an error if:
83    /// - `Cargo.toml` does not exist.
84    /// - The file cannot be read due to I/O issues.
85    /// - The `features` section in `Cargo.toml` is invalid.
86    ///
87    /// # Example
88    /// ```rust
89    /// use cargo_wrap::ProjectSettings;
90    /// let settings = ProjectSettings::new("/path/to/project", None, None, false);
91    /// match settings.get_features() {
92    ///     Ok(features) => println!("Available features: {:?}", features),
93    ///     Err(e) => eprintln!("Error retrieving features: {}", e),
94    /// }
95    /// ```
96    pub fn get_features(&self) -> io::Result<Vec<String>> {
97        let cargo_content = fs::read_to_string(&self.cargo_toml_path)?;
98        let parsed_toml: Value = cargo_content.parse().map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
99        if let Some(features) = parsed_toml.get("features").and_then(|f| f.as_table()) {
100            Ok(features.keys().cloned().collect())
101        } else {
102            Ok(vec![])
103        }
104    }
105
106    /// Marks the project to be built as `release`
107    pub fn set_release(&mut self) {
108        self.release = true;
109    }
110
111    /// Manually enable a feature that's available in the project
112    pub fn add_feature(&mut self, feature: String) {
113        self.features.get_or_insert_with(Vec::new).push(feature)
114    }
115}
116
117/// The main struct responsible for building a Rust project.
118///
119/// `Builder` acts as a wrapper around the `cargo build` command, allowing
120/// users to configure build settings such as verbosity, threading, and output paths.
121///
122/// # Fields
123///
124/// * `cargo_path` - Path to the `cargo` binary.
125/// * `project_settings` - The `ProjectSettings` instance containing build configurations.
126/// * `thread_count` - Optional number of jobs (`--jobs N`) to use during the build. Default value is 0.
127/// * `output_path` - Optional log file to store output.
128/// * `verbose_build` - If `true`, enables verbose output (`--verbose`).
129/// * `additional_flags` - Optional flags to pass to the `rustc` binary (via the `RUSTFLAGS` environment variable)
130#[derive(Default, Debug)]
131pub struct Builder {
132    cargo_path: PathBuf,
133    project_settings: ProjectSettings,
134    thread_count: usize,
135    output_path: Option<PathBuf>,
136    verbose_build: bool,
137    additional_flags: Vec<String>
138}
139
140impl Builder {
141
142    /// Private function to get the `cargo` binary path from the environment
143    fn get_cargo_path() -> io::Result<PathBuf> {
144        env::var_os("CARGO")
145            .map(PathBuf::from)
146            .ok_or_else(|| Error::new(ErrorKind::NotFound, "CARGO environment variable not found"))
147    }
148
149    /// Creates a new `Builder` instance for managing and executing cargo builds.
150    ///
151    /// This function initializes the builder with the given project settings,
152    /// allowing for configuration of build parameters such as job count and log output.
153    ///
154    /// # Arguments
155    ///
156    /// * `project_settings` - A `ProjectSettings` instance containing the configuration
157    ///   for the Rust project to be built.
158    /// * `thread_count` - Optional number of parallel jobs (`--jobs N`) to use for building.
159    ///   If `0`, the default job count will be used.
160    /// * `output_path` - Optional path to a log file where build output will be stored.
161    ///
162    /// # Returns
163    ///
164    /// * `Ok(Builder)` - A new `Builder` instance ready to execute a build.
165    /// * `Err(io::Error)` - If the `cargo` binary is not found in the environment.
166    ///
167    /// # Errors
168    ///
169    /// This function will return an error if:
170    /// - The `CARGO` environment variable is not set, meaning `cargo` cannot be found.
171    /// - The provided `output_path` is invalid or cannot be written to.
172    ///
173    /// # Example
174    /// ```rust
175    /// use cargo_wrap::{Builder, ProjectSettings};
176    /// use std::io;
177    ///
178    /// fn main() -> io::Result<()> {
179    ///     let settings = ProjectSettings::new("/path/to/project", None, None, false);
180    ///     let builder = Builder::new(settings, 4, Some("build.log"))?;
181    ///     Ok(())
182    /// }
183    pub fn new(project_settings: ProjectSettings, thread_count: usize, output_path:
184    Option<impl Into<PathBuf>>) ->
185               io::Result<Builder> {
186        let cargo_path = Builder::get_cargo_path()?;
187        Ok(Self {
188            cargo_path,
189            project_settings,
190            thread_count,
191            output_path: output_path.map(Into::into),
192            ..Default::default()
193        })
194    }
195
196    /// Tells the builder to use the `--verbose` flag when building
197    pub fn set_verbose(&mut self) {
198        self.verbose_build = true;
199    }
200
201    /// Adds a flag to the list of additional flags that will be passed to `rustc`
202    pub fn add_rustc_flag(&mut self, flag: String) {
203        self.additional_flags.push(flag);
204    }
205
206    /// Executes the build process using `cargo build`.
207    ///
208    /// This function spawns a `cargo build` process with the specified settings,
209    /// such as release/debug mode, enabled features, and output directories.
210    ///
211    /// # Returns
212    ///
213    /// * `Ok(())` - If the build succeeds.
214    /// * `Err(io::Error)` - If the build process fails.
215    ///
216    /// # Errors
217    ///
218    /// This function will return an error if:
219    /// - The `cargo` binary is missing from the system.
220    /// - The build process fails (e.g., compilation errors).
221    /// - The log file cannot be written to (if logging is enabled).
222    ///
223    /// # Example
224    /// ```rust
225    /// use cargo_wrap::{Builder, ProjectSettings};
226    /// use std::io;
227    ///
228    /// fn main() -> io::Result<()> {
229    ///     let settings = ProjectSettings::new("/path/to/project", None, None, false);
230    ///     let builder = Builder::new(settings, 4, Some("build.log"))?;
231    ///     builder.build()?;
232    ///     Ok(())
233    /// }
234    /// ```
235    pub fn build(&self) -> io::Result<()> {
236        let mut command = Command::new(self.cargo_path.clone());
237        command.arg("build");
238        if self.verbose_build {
239            command.arg("--verbose");
240        }
241        if self.project_settings.release {
242            command.arg("--release");
243        }
244        if self.thread_count > 0 {
245            command.arg("--jobs").arg(self.thread_count.to_string());
246
247        }
248        if let Some(output_path) = &self.project_settings.output_path {
249            command.env("CARGO_TARGET_DIR", output_path);
250        }
251        if !self.additional_flags.is_empty() {
252            command.env("RUSTFLAGS", self.additional_flags.join(" "));
253        }
254        if let Some(ref target) = self.project_settings.compilation_target {
255            command.arg("--target").arg(target);
256        }
257        if let Some(features) = &self.project_settings.features {
258            command.arg("--features");
259            features.iter().for_each(|f| { command.arg(f); });
260        }
261        if self.project_settings.no_default_features {
262            command.arg("--no-default-features");
263        }
264        if let Some(target) = &self.project_settings.target {
265            command.arg(if self.project_settings.is_lib { "--lib" } else { "--bin" }).arg(target);
266        }
267
268        let output = command.current_dir(&self.project_settings.project_path).output()?;
269        if let Some(output_log) = &self.output_path {
270            let mut output_file = OpenOptions::new().create(true).append(true).open(output_log)?;
271            output_file.write_all(&output.stdout)?;
272            output_file.write_all(&output.stderr)?;
273        }
274        if output.status.success() {
275            Ok(())
276        } else {
277            Err(Error::new(ErrorKind::Other, format!("Failed to compile project: {}", output.status)))
278        }
279    }
280}
281