tmc_langs_plugins/
lib.rs

1#![deny(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_used)]
2
3//! Abstracts over the various language plugins.
4
5pub mod compression;
6mod error;
7
8use blake3::Hash;
9pub use error::PluginError;
10use std::{
11    io::{Read, Seek},
12    path::{Path, PathBuf},
13};
14pub use tmc_langs_csharp::CSharpPlugin;
15use tmc_langs_framework::{Archive, LanguagePlugin, TmcError};
16pub use tmc_langs_framework::{
17    Compression, ExerciseDesc, ExercisePackagingConfiguration, Language,
18    NothingIsStudentFilePolicy, RunResult, StudentFilePolicy, StyleValidationResult,
19    StyleValidationStrategy,
20};
21// the Java plugin is disabled on musl
22#[cfg(not(target_env = "musl"))]
23pub use tmc_langs_java::{AntPlugin, MavenPlugin};
24pub use tmc_langs_make::MakePlugin;
25pub use tmc_langs_notests::NoTestsPlugin;
26pub use tmc_langs_python3::Python3Plugin;
27pub use tmc_langs_r::RPlugin;
28
29/// Finds the correct language plug-in for the given exercise path and calls `LanguagePlugin::extract_project`,
30/// If no language plugin matches, see `extract_project_overwrite`.
31pub fn extract_project(
32    compressed_project: impl std::io::Read + std::io::Seek,
33    target_location: &Path,
34    compression: Compression,
35    clean: bool,
36) -> Result<(), PluginError> {
37    let mut archive = Archive::new(compressed_project, compression)?;
38    if let Ok(plugin) = PluginType::from_exercise(target_location) {
39        plugin.extract_project(&mut archive, target_location, clean)?;
40    } else if let Ok(plugin) = PluginType::from_archive(&mut archive) {
41        plugin.extract_project(&mut archive, target_location, clean)?;
42    } else {
43        log::debug!("no matching language plugin found",);
44        archive.extract(target_location)?;
45    }
46    Ok(())
47}
48
49/// Compresses the directory at the given path, only including student files unless `naive` is set to true.
50pub fn compress_project(
51    path: &Path,
52    compression: Compression,
53    deterministic: bool,
54    naive: bool,
55    hash: bool,
56    size_limit_mb: u32,
57) -> Result<(Vec<u8>, Option<Hash>), PluginError> {
58    let (compressed, hash) = if naive {
59        compression.compress(path, hash)?
60    } else {
61        let policy = get_student_file_policy(path)?;
62        compression::compress_student_files(
63            policy.as_ref(),
64            path,
65            compression,
66            deterministic,
67            hash,
68            size_limit_mb,
69        )?
70    };
71    Ok((compressed, hash))
72}
73
74/// Enum containing variants for each language plugin.
75pub enum Plugin {
76    CSharp(CSharpPlugin),
77    Make(MakePlugin),
78    // the Java plugin is disabled on musl
79    #[cfg(not(target_env = "musl"))]
80    Maven(MavenPlugin),
81    NoTests(NoTestsPlugin),
82    Python3(Python3Plugin),
83    R(RPlugin),
84    // the Java plugin is disabled on musl
85    #[cfg(not(target_env = "musl"))]
86    Ant(AntPlugin),
87}
88
89impl Plugin {
90    // Get language plugin for the given path.
91    pub fn from_exercise(path: &Path) -> Result<Self, PluginError> {
92        let plugin = match PluginType::from_exercise(path)? {
93            PluginType::NoTests => Plugin::NoTests(NoTestsPlugin::new()),
94            PluginType::CSharp => Plugin::CSharp(CSharpPlugin::new()),
95            PluginType::Make => Plugin::Make(MakePlugin::new()),
96            PluginType::Python3 => Plugin::Python3(Python3Plugin::new()),
97            PluginType::R => Plugin::R(RPlugin::new()),
98            // the Java plugin is disabled on musl
99            #[cfg(not(target_env = "musl"))]
100            PluginType::Maven => Plugin::Maven(MavenPlugin::new()?),
101            // the Java plugin is disabled on musl
102            #[cfg(not(target_env = "musl"))]
103            PluginType::Ant => Plugin::Ant(AntPlugin::new()?),
104        };
105        Ok(plugin)
106    }
107
108    pub fn clean(&self, path: &Path) -> Result<(), TmcError> {
109        match self {
110            Plugin::CSharp(plugin) => plugin.clean(path),
111            Plugin::Make(plugin) => plugin.clean(path),
112            // the Java plugin is disabled on musl
113            #[cfg(not(target_env = "musl"))]
114            Plugin::Maven(plugin) => plugin.clean(path),
115            Plugin::NoTests(plugin) => plugin.clean(path),
116            Plugin::Python3(plugin) => plugin.clean(path),
117            Plugin::R(plugin) => plugin.clean(path),
118            // the Java plugin is disabled on musl
119            #[cfg(not(target_env = "musl"))]
120            Plugin::Ant(plugin) => plugin.clean(path),
121        }
122    }
123
124    pub fn scan_exercise(
125        &self,
126        path: &Path,
127        exercise_name: String,
128    ) -> Result<ExerciseDesc, TmcError> {
129        match self {
130            Plugin::CSharp(plugin) => plugin.scan_exercise(path, exercise_name),
131            Plugin::Make(plugin) => plugin.scan_exercise(path, exercise_name),
132            // the Java plugin is disabled on musl
133            #[cfg(not(target_env = "musl"))]
134            Plugin::Maven(plugin) => plugin.scan_exercise(path, exercise_name),
135            Plugin::NoTests(plugin) => plugin.scan_exercise(path, exercise_name),
136            Plugin::Python3(plugin) => plugin.scan_exercise(path, exercise_name),
137            Plugin::R(plugin) => plugin.scan_exercise(path, exercise_name),
138            // the Java plugin is disabled on musl
139            #[cfg(not(target_env = "musl"))]
140            Plugin::Ant(plugin) => plugin.scan_exercise(path, exercise_name),
141        }
142    }
143
144    pub fn run_tests(&self, path: &Path) -> Result<RunResult, TmcError> {
145        match self {
146            Plugin::CSharp(plugin) => plugin.run_tests(path),
147            Plugin::Make(plugin) => plugin.run_tests(path),
148            // the Java plugin is disabled on musl
149            #[cfg(not(target_env = "musl"))]
150            Plugin::Maven(plugin) => plugin.run_tests(path),
151            Plugin::NoTests(plugin) => plugin.run_tests(path),
152            Plugin::Python3(plugin) => plugin.run_tests(path),
153            Plugin::R(plugin) => plugin.run_tests(path),
154            // the Java plugin is disabled on musl
155            #[cfg(not(target_env = "musl"))]
156            Plugin::Ant(plugin) => plugin.run_tests(path),
157        }
158    }
159
160    pub fn check_code_style(
161        &self,
162        path: &Path,
163        locale: Language,
164    ) -> Result<Option<StyleValidationResult>, TmcError> {
165        match self {
166            Plugin::CSharp(plugin) => plugin.check_code_style(path, locale),
167            Plugin::Make(plugin) => plugin.check_code_style(path, locale),
168            // the Java plugin is disabled on musl
169            #[cfg(not(target_env = "musl"))]
170            Plugin::Maven(plugin) => plugin.check_code_style(path, locale),
171            Plugin::NoTests(plugin) => plugin.check_code_style(path, locale),
172            Plugin::Python3(plugin) => plugin.check_code_style(path, locale),
173            Plugin::R(plugin) => plugin.check_code_style(path, locale),
174            // the Java plugin is disabled on musl
175            #[cfg(not(target_env = "musl"))]
176            Plugin::Ant(plugin) => plugin.check_code_style(path, locale),
177        }
178    }
179}
180
181/// Allows calling LanguagePlugin functions without constructing the plugin.
182#[derive(Clone, Copy)]
183pub enum PluginType {
184    CSharp,
185    Make,
186    // the Java plugin is disabled on musl
187    #[cfg(not(target_env = "musl"))]
188    Maven,
189    NoTests,
190    Python3,
191    R,
192    // the Java plugin is disabled on musl
193    #[cfg(not(target_env = "musl"))]
194    Ant,
195}
196
197macro_rules! delegate_plugin_type {
198    ($self:ident, $($args:tt)*) => {
199        match $self {
200            Self::CSharp => CSharpPlugin::$($args)*,
201            Self::Make => MakePlugin::$($args)*,
202            // the Java plugin is disabled on musl
203            #[cfg(not(target_env = "musl"))]
204            Self::Maven => MavenPlugin::$($args)*,
205            Self::NoTests => NoTestsPlugin::$($args)*,
206            Self::Python3 => Python3Plugin::$($args)*,
207            Self::R => RPlugin::$($args)*,
208            // the Java plugin is disabled on musl
209            #[cfg(not(target_env = "musl"))]
210            Self::Ant => AntPlugin::$($args)*,
211        }
212    };
213}
214
215impl PluginType {
216    pub fn from_exercise(path: &Path) -> Result<Self, PluginError> {
217        let (plugin_name, plugin_type) = if NoTestsPlugin::is_exercise_type_correct(path) {
218            (NoTestsPlugin::PLUGIN_NAME, PluginType::NoTests)
219        } else if CSharpPlugin::is_exercise_type_correct(path) {
220            (CSharpPlugin::PLUGIN_NAME, PluginType::CSharp)
221        } else if MakePlugin::is_exercise_type_correct(path) {
222            (MakePlugin::PLUGIN_NAME, PluginType::Make)
223        } else if Python3Plugin::is_exercise_type_correct(path) {
224            (Python3Plugin::PLUGIN_NAME, PluginType::Python3)
225        } else if RPlugin::is_exercise_type_correct(path) {
226            (RPlugin::PLUGIN_NAME, PluginType::R)
227        } else {
228            // the Java plugin is disabled on musl
229            #[cfg(not(target_env = "musl"))]
230            if MavenPlugin::is_exercise_type_correct(path) {
231                (MavenPlugin::PLUGIN_NAME, PluginType::Maven)
232            } else if AntPlugin::is_exercise_type_correct(path) {
233                // TODO: currently, ant needs to be last because any project with src and test are recognized as ant
234                (AntPlugin::PLUGIN_NAME, PluginType::Ant)
235            } else {
236                return Err(PluginError::PluginNotFound(path.to_path_buf()));
237            }
238            #[cfg(target_env = "musl")]
239            return Err(PluginError::PluginNotFound(path.to_path_buf()));
240        };
241        log::info!("Detected project at {} as {}", path.display(), plugin_name);
242        Ok(plugin_type)
243    }
244
245    pub fn from_archive<R: Read + Seek>(archive: &mut Archive<R>) -> Result<Self, PluginError> {
246        let (plugin_name, plugin_type) = if NoTestsPlugin::is_archive_type_correct(archive) {
247            (NoTestsPlugin::PLUGIN_NAME, PluginType::NoTests)
248        } else if CSharpPlugin::is_archive_type_correct(archive) {
249            (CSharpPlugin::PLUGIN_NAME, PluginType::CSharp)
250        } else if MakePlugin::is_archive_type_correct(archive) {
251            (MakePlugin::PLUGIN_NAME, PluginType::Make)
252        } else if Python3Plugin::is_archive_type_correct(archive) {
253            (Python3Plugin::PLUGIN_NAME, PluginType::Python3)
254        } else if RPlugin::is_archive_type_correct(archive) {
255            (RPlugin::PLUGIN_NAME, PluginType::R)
256        } else {
257            // the Java plugin is disabled on musl
258            #[cfg(not(target_env = "musl"))]
259            if MavenPlugin::is_archive_type_correct(archive) {
260                (MavenPlugin::PLUGIN_NAME, PluginType::Maven)
261            } else if AntPlugin::is_archive_type_correct(archive) {
262                // TODO: currently, ant needs to be last because any project with src and test are recognized as ant
263                (AntPlugin::PLUGIN_NAME, PluginType::Ant)
264            } else {
265                return Err(PluginError::PluginNotFoundInArchive);
266            }
267            #[cfg(target_env = "musl")]
268            return Err(PluginError::PluginNotFoundInArchive);
269        };
270        log::info!("Detected project in archive as {plugin_name}");
271        Ok(plugin_type)
272    }
273
274    pub fn get_exercise_packaging_configuration(
275        self,
276        exercise_path: &Path,
277    ) -> Result<ExercisePackagingConfiguration, TmcError> {
278        delegate_plugin_type!(self, get_exercise_packaging_configuration(exercise_path))
279    }
280
281    pub fn extract_project<R: Read + Seek>(
282        self,
283        archive: &mut Archive<R>,
284        target_location: &Path,
285        clean: bool,
286    ) -> Result<(), TmcError> {
287        delegate_plugin_type!(self, extract_project(archive, target_location, clean))
288    }
289
290    pub fn extract_student_files(
291        self,
292        compressed_project: impl std::io::Read + std::io::Seek,
293        compression: Compression,
294        target_location: &Path,
295    ) -> Result<(), TmcError> {
296        delegate_plugin_type!(
297            self,
298            extract_student_files(compressed_project, compression, target_location)
299        )
300    }
301
302    pub fn find_project_dir_in_archive<R: Read + Seek>(
303        self,
304        archive: &mut Archive<R>,
305    ) -> Result<PathBuf, TmcError> {
306        delegate_plugin_type!(self, find_project_dir_in_archive(archive))
307    }
308
309    pub fn safe_find_project_dir_in_archive<R: Read + Seek>(
310        self,
311        archive: &mut Archive<R>,
312    ) -> Result<PathBuf, TmcError> {
313        Ok(delegate_plugin_type!(
314            self,
315            safe_find_project_dir_in_archive(archive)
316        ))
317    }
318
319    pub fn get_available_points(self, exercise_path: &Path) -> Result<Vec<String>, TmcError> {
320        delegate_plugin_type!(self, get_available_points(exercise_path))
321    }
322}
323
324pub fn get_student_file_policy(path: &Path) -> Result<Box<dyn StudentFilePolicy>, PluginError> {
325    let policy: Box<dyn StudentFilePolicy> = match PluginType::from_exercise(path)? {
326        PluginType::NoTests => Box::new(<NoTestsPlugin as LanguagePlugin>::StudentFilePolicy::new(
327            path,
328        )?),
329        PluginType::CSharp => Box::new(<CSharpPlugin as LanguagePlugin>::StudentFilePolicy::new(
330            path,
331        )?),
332        PluginType::Make => Box::new(<MakePlugin as LanguagePlugin>::StudentFilePolicy::new(
333            path,
334        )?),
335        PluginType::Python3 => Box::new(<Python3Plugin as LanguagePlugin>::StudentFilePolicy::new(
336            path,
337        )?),
338        PluginType::R => Box::new(<RPlugin as LanguagePlugin>::StudentFilePolicy::new(path)?),
339        // the Java plugin is disabled on musl
340        #[cfg(not(target_env = "musl"))]
341        PluginType::Maven => Box::new(<MavenPlugin as LanguagePlugin>::StudentFilePolicy::new(
342            path,
343        )?),
344        // the Java plugin is disabled on musl
345        #[cfg(not(target_env = "musl"))]
346        PluginType::Ant => Box::new(<AntPlugin as LanguagePlugin>::StudentFilePolicy::new(path)?),
347    };
348    Ok(policy)
349}