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