1use core::error::Error;
2use std::ffi::OsStr;
3use std::path::Path;
4use std::process::Command;
5use std::{env, io};
6
7use cargo_metadata::camino::Utf8PathBuf;
8
9pub fn build() -> Result<(), BuildError> {
40 let Some(_g) = BuildGuard::<EnvVarGuard>::new()? else { return Ok(()) };
41 let compilation_opts = CompilationOpts::from_env()?;
42 let manifest_path = EnvVar::get("CARGO_MANIFEST_PATH")?;
43 let manifest = CargoManifest::from_path(manifest_path.as_str())?;
44 let features = EnabledFeatures::from_env(&manifest)?;
45 BuildCommand::new(&manifest, &compilation_opts, &features).exec()?;
46 println!(
47 "cargo:rustc-env={}={}",
48 manifest.profile_env(),
49 compilation_opts.profile.as_str()
50 );
51 println!(
53 "cargo:rerun-if-changed=\"{}\"",
54 manifest.library_path(compilation_opts.profile.as_str()),
55 );
56 Ok(())
57}
58
59#[derive(Debug, thiserror::Error)]
61#[error(transparent)]
62pub struct BuildError {
63 #[from]
64 kind: BuildErrorKind,
65}
66
67pub(super) struct CargoManifest {
68 metadata: cargo_metadata::Metadata,
69}
70
71struct BuildGuard<G: Guard> {
72 guard: Option<G>,
73}
74
75struct EnvVarGuard;
76
77struct CompilationOpts {
78 profile: Profile,
79}
80
81enum Profile {
82 Debug,
83 Release,
84 Other(EnvVar),
85}
86
87struct EnabledFeatures {
88 features: Vec<String>,
89}
90
91struct BuildCommand {
92 command: Command,
93}
94
95impl EnvVarGuard {
96 const NAME: &'static str = "NVIM_OXI_BUILDING_TESTS";
97}
98
99struct EnvVar(String);
100
101#[derive(Debug, thiserror::Error)]
102enum BuildErrorKind {
103 #[error("couldn't build tests: {0}")]
104 Build(io::Error),
105
106 #[error("couldn't acquire guard: {0}")]
107 CouldntAcquireGuard(Box<dyn Error>),
108
109 #[error("couldn't read manifest: {0}")]
110 CouldntReadManifest(cargo_metadata::Error),
111
112 #[error("nvim_oxi::tests::build() can only be used inside a build script")]
113 NotInBuildScript,
114
115 #[error("couldn't get the root package")]
116 NoRootPackage,
117}
118
119impl<G: Guard> BuildGuard<G> {
120 fn new() -> Result<Option<Self>, BuildError> {
121 match G::acquire() {
122 Ok(guard) => Ok(Some(Self { guard: Some(guard) })),
123 Err(Ok(_busy)) => Ok(None),
124 Err(Err(acquire_err)) => {
125 Err(BuildErrorKind::CouldntAcquireGuard(Box::new(acquire_err))
126 .into())
127 },
128 }
129 }
130}
131
132impl CompilationOpts {
133 fn from_env() -> Result<Self, BuildError> {
134 Ok(Self { profile: Profile::from_env_var(EnvVar::get("PROFILE")?) })
135 }
136}
137
138impl Profile {
139 fn as_args(&self) -> Vec<impl AsRef<OsStr> + '_> {
140 enum Arg<'a> {
141 Str(&'a str),
142 EnvVar(&'a EnvVar),
143 }
144
145 impl AsRef<OsStr> for Arg<'_> {
146 fn as_ref(&self) -> &OsStr {
147 match self {
148 Arg::Str(s) => s.as_ref(),
149 Arg::EnvVar(s) => s.as_str().as_ref(),
150 }
151 }
152 }
153
154 match self {
155 Profile::Debug => vec![],
156 Profile::Release => vec![Arg::Str("--release")],
157 Profile::Other(other) => {
158 vec![Arg::Str("--profile"), Arg::EnvVar(other)]
159 },
160 }
161 }
162
163 fn as_str(&self) -> &str {
164 match self {
165 Profile::Debug => "debug",
166 Profile::Release => "release",
167 Profile::Other(other) => other.as_str(),
168 }
169 }
170
171 fn from_env_var(profile: EnvVar) -> Self {
172 match profile.as_str() {
173 "debug" => Self::Debug,
174 "release" => Self::Release,
175 _ => Self::Other(profile),
176 }
177 }
178}
179
180impl CargoManifest {
181 pub(super) fn from_path(
182 path: impl AsRef<Path>,
183 ) -> Result<Self, BuildError> {
184 let metadata = cargo_metadata::MetadataCommand::new()
185 .manifest_path(path.as_ref())
186 .exec()
187 .map_err(BuildErrorKind::CouldntReadManifest)?;
188
189 if metadata.root_package().is_none() {
190 return Err(BuildErrorKind::NoRootPackage.into());
191 }
192
193 Ok(Self { metadata })
194 }
195
196 pub(super) fn profile_env(&self) -> String {
199 format!(
200 "NVIM_OXI_TEST_BUILD_PROFILE_{}",
201 self.root_package().name.to_ascii_uppercase().replace('-', "_")
202 )
203 }
204
205 pub(super) fn target_dir(&self) -> Utf8PathBuf {
208 self.metadata
209 .target_directory
210 .join("nvim_oxi_tests")
215 .join(&self.root_package().name)
218 }
219
220 pub(super) fn library_path(&self, profile_name: &str) -> Utf8PathBuf {
221 let library_name = format!(
222 "{prefix}{crate_name}{suffix}",
223 prefix = env::consts::DLL_PREFIX,
224 suffix = env::consts::DLL_SUFFIX,
225 crate_name = self.root_package().name.replace('-', "_"),
226 );
227 self.target_dir().join(profile_name).join(library_name)
228 }
229
230 fn root_package(&self) -> &cargo_metadata::Package {
231 self.metadata.root_package().expect("checked in `from_path()`")
232 }
233}
234
235impl EnabledFeatures {
236 fn from_env(manifest: &CargoManifest) -> Result<Self, BuildError> {
237 let mut features = Vec::new();
238
239 for feature in manifest.root_package().features.keys() {
240 let env = format!(
241 "CARGO_FEATURE_{}",
242 feature.to_ascii_uppercase().replace('-', "_")
243 );
244 if EnvVar::get(&env).is_ok() {
245 features.push(feature.clone());
246 }
247 }
248
249 Ok(Self { features })
250 }
251}
252
253impl BuildCommand {
254 fn exec(mut self) -> Result<(), BuildError> {
255 self.command
256 .status()
257 .map(|_| ())
258 .map_err(|io_err| BuildErrorKind::Build(io_err).into())
259 }
260
261 fn new(
262 manifest: &CargoManifest,
263 compilation_opts: &CompilationOpts,
264 enabled_features: &EnabledFeatures,
265 ) -> Self {
266 let mut command = Command::new("cargo");
267 command
268 .arg("build")
269 .args(compilation_opts.profile.as_args())
270 .args(["--target-dir", manifest.target_dir().as_str()])
271 .arg("--no-default-features")
272 .arg("--features")
273 .arg(enabled_features.features.join(","));
274 Self { command }
275 }
276}
277
278impl EnvVar {
279 fn as_str(&self) -> &str {
280 &self.0
281 }
282
283 fn get(env: &str) -> Result<Self, BuildError> {
284 match env::var(env) {
285 Ok(value) => Ok(Self(value)),
286 Err(_) => Err(BuildErrorKind::NotInBuildScript.into()),
287 }
288 }
289}
290
291impl Guard for EnvVarGuard {
292 type Error = env::VarError;
293
294 fn acquire() -> Result<Self, Result<GuardBusy, Self::Error>> {
295 match env::var(Self::NAME) {
296 Ok(_) => Err(Ok(GuardBusy)),
297 Err(env::VarError::NotPresent) => unsafe {
298 env::set_var(Self::NAME, "1");
299 Ok(Self)
300 },
301 Err(var_error) => Err(Err(var_error)),
302 }
303 }
304
305 fn release(self) -> Result<(), Self::Error> {
306 Ok(())
308 }
309}
310
311impl<G: Guard> Drop for BuildGuard<G> {
312 fn drop(&mut self) {
313 if let Err(err) = self.guard.take().unwrap().release() {
314 panic!("couldn't release guard: {err}");
315 }
316 }
317}
318
319trait Guard: Sized {
320 type Error: Error + 'static;
321 fn acquire() -> Result<Self, Result<GuardBusy, Self::Error>>;
322 fn release(self) -> Result<(), Self::Error>;
323}
324
325struct GuardBusy;