1#![warn(clippy::all)]
2
3use std::fmt;
56use std::io::{Read as _, Write};
57
58use anyhow::{anyhow, Context as _};
59use lazy_static::lazy_static;
60use semver::{Version, VersionReq};
61use serde::{Deserialize, Serialize};
62use tokio::process::Command;
63
64use acick_util::{abs_path, console, model, DATA_LOCAL_DIR};
65
66mod session_config;
67mod template;
68
69use crate::abs_path::AbsPathBuf;
70use crate::console::Console;
71use crate::model::{Contest, ContestId, LangName, Problem, ProblemId, Service, ServiceKind};
72pub use session_config::SessionConfig;
73use template::{Expand, ProblemTempl, Shell, TargetContext, TargetTempl};
74
75pub type Error = anyhow::Error;
76pub type Result<T> = anyhow::Result<T>;
77
78lazy_static! {
79 static ref VERSION: Version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
80}
81
82#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
83pub struct Config {
84 pub service_id: ServiceKind,
85 pub contest_id: ContestId,
86 pub base_dir: AbsPathBuf,
87 body: ConfigBody,
88}
89
90impl Config {
91 pub fn load(
92 service_id: ServiceKind,
93 contest_id: ContestId,
94 base_dir: Option<AbsPathBuf>,
95 cnsl: &mut Console,
96 ) -> Result<Self> {
97 let base_dir = match base_dir {
98 Some(base_dir) => base_dir,
99 None => ConfigBody::search(cnsl)?,
100 };
101 let body = ConfigBody::load(&base_dir, cnsl)?;
102 Ok(Self {
103 service_id,
104 contest_id,
105 base_dir,
106 body,
107 })
108 }
109
110 pub fn session(&self) -> &SessionConfig {
111 &self.body.session
112 }
113
114 pub fn service(&self) -> &ServiceConfig {
115 self.body.services.get(self.service_id)
116 }
117
118 pub fn move_testcases_dir(
119 &self,
120 problem: &Problem,
121 from: &AbsPathBuf,
122 cnsl: &mut Console,
123 ) -> Result<bool> {
124 let testcases_abs_dir = self.testcases_abs_dir(problem.id())?;
125 if testcases_abs_dir.as_ref().exists() {
126 let message = format!(
127 "remove existing testcases dir {}?",
128 testcases_abs_dir.strip_prefix(&self.base_dir).display()
129 );
130 if !cnsl.confirm(&message, false)? {
131 return Ok(false);
132 }
133 testcases_abs_dir.remove_dir_all_pretty(Some(&self.base_dir), cnsl)?;
134 } else if let Some(parent) = testcases_abs_dir.parent() {
135 parent.create_dir_all()?;
136 }
137
138 testcases_abs_dir.move_from_pretty(from, Some(&self.base_dir), cnsl)?;
139
140 Ok(true)
141 }
142
143 pub fn save_problem(
144 &self,
145 problem: &Problem,
146 overwrite: bool,
147 cnsl: &mut Console,
148 ) -> Result<Option<bool>> {
149 let problem_abs_path = self.problem_abs_path(problem.id())?;
150 problem_abs_path.save_pretty(
151 |file| serde_yaml::to_writer(file, &problem).context("Could not save problem as yaml"),
152 overwrite,
153 Some(&self.base_dir),
154 cnsl,
155 )
156 }
157
158 pub fn load_problem(&self, problem_id: &ProblemId, cnsl: &mut Console) -> Result<Problem> {
159 let problem_abs_path = self.problem_abs_path(problem_id)?;
160 let problem: Problem = problem_abs_path
161 .load_pretty(
162 |file| serde_yaml::from_reader(file).context("Could not read problem as yaml"),
163 Some(&self.base_dir),
164 cnsl,
165 )
166 .context(
167 "Could not load problem file. \
168 Fetch problem data first by `acick fetch` command.",
169 )?;
170 if problem.id() != problem_id {
171 Err(anyhow!(
172 "Found mismatching problem id in problem file : {}",
173 problem.id()
174 ))
175 } else {
176 Ok(problem)
177 }
178 }
179
180 pub fn expand_and_save_source(
181 &self,
182 service: &Service,
183 contest: &Contest,
184 problem: &Problem,
185 overwrite: bool,
186 cnsl: &mut Console,
187 ) -> Result<Option<bool>> {
188 if service.id() != self.service_id || contest.id() != &self.contest_id {
189 return Err(anyhow!("Found mismatching service id or contest id"));
190 }
191 let source_abs_path = self.source_abs_path(problem.id())?;
192 let template = match &self.service().template {
193 Some(template) => template,
194 None => return Ok(None), };
196 let template_expanded = template.expand_with(service, contest, problem)?;
197 source_abs_path.save_pretty(
198 |mut file| Ok(file.write_all(template_expanded.as_bytes())?),
199 overwrite,
200 Some(&self.base_dir),
201 cnsl,
202 )
203 }
204
205 pub fn load_source(&self, problem_id: &ProblemId, cnsl: &mut Console) -> Result<String> {
206 let source_abs_path = self.source_abs_path(problem_id)?;
207 source_abs_path.load_pretty(
208 |mut file| {
209 let mut buf = String::new();
210 file.read_to_string(&mut buf)?;
211 Ok(buf)
212 },
213 Some(&self.base_dir),
214 cnsl,
215 )
216 }
217
218 pub fn exec_compile(&self, problem_id: &ProblemId) -> Result<Command> {
219 let compile = &self.service().compile;
220 self.exec_templ(compile, problem_id)
221 }
222
223 pub fn exec_run(&self, problem_id: &ProblemId) -> Result<Command> {
224 let run = &self.service().run;
225 self.exec_templ(run, problem_id)
226 }
227
228 fn problem_abs_path(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
229 let problem_path = &self.body.problem_path;
230 self.expand_to_abs(problem_path, problem_id)
231 }
232
233 pub fn testcases_abs_dir(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
234 let testcases_dir = &self.body.testcases_dir;
235 self.expand_to_abs(testcases_dir, problem_id)
236 }
237
238 fn working_abs_dir(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
239 let working_dir = &self.service().working_dir;
240 self.expand_to_abs(working_dir, problem_id)
241 }
242
243 fn source_abs_path(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
244 let source_path = &self.service().source_path;
245 self.expand_to_abs(source_path, problem_id)
246 }
247
248 fn expand_to_abs(&self, path: &TargetTempl, problem_id: &ProblemId) -> Result<AbsPathBuf> {
249 path.expand_with(self.service_id, &self.contest_id, problem_id)
250 .and_then(|path_expanded| self.base_dir.join_expand(path_expanded))
251 }
252
253 fn exec_templ<'a, T: Expand<'a>>(
254 &'a self,
255 templ: &T,
256 problem_id: &'a ProblemId,
257 ) -> Result<Command>
258 where
259 T: Expand<'a, Context = TargetContext<'a>>,
260 {
261 let target_context = TargetContext::new(self.service_id, &self.contest_id, problem_id);
262 let working_abs_dir = self.working_abs_dir(problem_id)?;
263 let mut command = self.body.shell.exec_templ(templ, &target_context)?;
264 command.current_dir(working_abs_dir.as_ref());
265 Ok(command)
266 }
267
268 pub fn default_in_dir(base_dir: AbsPathBuf) -> Self {
269 let body = ConfigBody::default_in_dir(&base_dir);
270
271 Self {
272 service_id: ServiceKind::default(),
273 contest_id: Contest::default().id().clone(),
274 base_dir,
275 body,
276 }
277 }
278}
279
280impl fmt::Display for Config {
281 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282 let yaml_str = serde_yaml::to_string(self).map_err(|_| fmt::Error)?;
283 write!(f, "{}", yaml_str)
284 }
285}
286
287#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
288pub struct ConfigBody {
289 #[serde(with = "string_serde")]
290 version: Version,
291 #[serde(default)]
292 shell: Shell,
293 #[serde(default = "ConfigBody::default_problem_path")]
294 problem_path: TargetTempl,
295 #[serde(default = "ConfigBody::default_testcases_dir")]
296 testcases_dir: TargetTempl,
297 #[serde(default)]
298 session: SessionConfig,
299 #[serde(default)]
300 services: ServicesConfig,
301}
302
303impl ConfigBody {
304 pub const FILE_NAME: &'static str = ".acick.yaml";
305
306 const DEFAULT_PROBLEM_PATH: &'static str =
307 "{{ service }}/{{ contest }}/{{ problem | lower }}/problem.yaml";
308
309 const DEFAULT_TESTCASES_DIR: &'static str =
310 "{{ service }}/{{ contest }}/{{ problem | lower }}/testcases";
311
312 pub fn generate_to(writer: &mut dyn Write) -> Result<()> {
313 writeln!(
314 writer,
315 include_str!("../resources/.acick.yaml.txt"),
316 version = &*VERSION,
317 bash = Shell::find_bash().display()
318 )
319 .context("Could not write config")
320 }
321
322 fn default_in_dir(base_dir: &AbsPathBuf) -> Self {
323 Self {
324 version: VERSION.clone(),
325 shell: Shell::default(),
326 problem_path: Self::default_problem_path(),
327 testcases_dir: Self::default_testcases_dir(),
328 session: SessionConfig::default_in_dir(base_dir),
329 services: ServicesConfig::default(),
330 }
331 }
332
333 fn default_problem_path() -> TargetTempl {
334 Self::DEFAULT_PROBLEM_PATH.into()
335 }
336
337 fn default_testcases_dir() -> TargetTempl {
338 Self::DEFAULT_TESTCASES_DIR.into()
339 }
340
341 fn search(cnsl: &mut Console) -> Result<AbsPathBuf> {
342 let cwd = AbsPathBuf::cwd()?;
343 let base_dir = cwd.search_dir_contains(Self::FILE_NAME).with_context(|| {
344 format!(
345 "Could not find config file ({}) in {} or any of the parent directories. \
346 Create config file first by `acick init` command.",
347 Self::FILE_NAME,
348 cwd
349 )
350 })?;
351 writeln!(cnsl, "Found config file in base_dir: {}", base_dir)?;
352 Ok(base_dir)
353 }
354
355 fn load(base_dir: &AbsPathBuf, cnsl: &mut Console) -> Result<Self> {
356 let body: Self = base_dir.join(Self::FILE_NAME).load_pretty(
357 |file| serde_yaml::from_reader(file).context("Could not read config file as yaml"),
358 Some(base_dir),
359 cnsl,
360 )?;
361 body.validate()?;
362 Ok(body)
363 }
364
365 fn validate(&self) -> Result<()> {
366 let version_req = VersionReq::parse(&self.version.to_string())
368 .context("Could not parse version requirement")?;
369 if !version_req.matches(&VERSION) {
370 return Err(anyhow!(
371 r#"Found mismatched version in config file.
372 config version: {}
373 acick version : {}
374Fix the config file so that it is compatible with the current version of acick."#,
375 self.version,
376 &*VERSION
377 ));
378 }
379
380 Ok(())
381 }
382}
383
384impl Default for ConfigBody {
385 fn default() -> Self {
386 Self {
387 version: VERSION.clone(),
388 shell: Shell::default(),
389 problem_path: Self::default_problem_path(),
390 testcases_dir: Self::default_testcases_dir(),
391 session: SessionConfig::default(),
392 services: ServicesConfig::default(),
393 }
394 }
395}
396
397#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
398#[serde(default)]
399pub struct ServicesConfig {
400 atcoder: ServiceConfig,
401}
402
403impl ServicesConfig {
404 fn get(&self, service_id: ServiceKind) -> &ServiceConfig {
405 match service_id {
406 ServiceKind::Atcoder => &self.atcoder,
407 }
408 }
409}
410
411impl Default for ServicesConfig {
412 fn default() -> Self {
413 Self {
414 atcoder: ServiceConfig::default_for(ServiceKind::Atcoder),
415 }
416 }
417}
418
419#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
420pub struct ServiceConfig {
421 lang_names: Vec<LangName>,
422 working_dir: TargetTempl,
423 source_path: TargetTempl,
424 compile: TargetTempl,
425 run: TargetTempl,
426 #[serde(default)]
427 template: Option<ProblemTempl>,
428}
429
430impl ServiceConfig {
431 const DEFAULT_TEMPLATE: &'static str = r#"/*
432[{{ contest.id }}] {{ problem.id }} - {{ problem.name }}
433*/
434
435#include <iostream>
436using namespace std;
437
438int main() {
439 return 0;
440}
441"#;
442
443 fn default_for(service_id: ServiceKind) -> Self {
444 match service_id {
445 ServiceKind::Atcoder => Self {
446 lang_names: vec!["C++ (GCC 9.2.1)".into(), "C++14 (GCC 5.4.1)".into()],
447 working_dir: "{{ service }}/{{ contest }}/{{ problem | lower }}".into(),
448 source_path: "{{ service }}/{{ contest }}/{{ problem | lower }}/Main.cpp".into(),
449 compile: "set -x && g++ -std=gnu++17 -Wall -Wextra -O2 -o ./a.out ./Main.cpp"
450 .into(),
451 run: "./a.out".into(),
453 template: Some(Self::DEFAULT_TEMPLATE.into()),
454 },
455 }
456 }
457
458 pub fn lang_names(&self) -> &[LangName] {
459 &self.lang_names
460 }
461}
462
463mod string_serde {
464 use std::fmt::Display;
465 use std::str::FromStr;
466
467 use serde::{de, Deserialize, Deserializer, Serializer};
468
469 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
470 where
471 T: Display,
472 S: Serializer,
473 {
474 serializer.collect_str(value)
475 }
476
477 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
478 where
479 T: FromStr,
480 T::Err: Display,
481 D: Deserializer<'de>,
482 {
483 String::deserialize(deserializer)?
484 .parse()
485 .map_err(de::Error::custom)
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use std::fs::OpenOptions;
492
493 use tempfile::tempdir;
494
495 use super::*;
496 use crate::template::TargetContext;
497
498 #[test]
499 fn generate_and_deserialize() -> anyhow::Result<()> {
500 let mut buf = Vec::new();
501 ConfigBody::generate_to(&mut buf)?;
502 let body_yaml_str = String::from_utf8(buf)?;
503 let body_generated: ConfigBody = serde_yaml::from_str(&body_yaml_str)?;
504
505 let body_default = ConfigBody::default();
506
507 assert_eq!(body_generated, body_default);
508
509 Ok(())
510 }
511
512 #[tokio::test]
513 async fn exec_default_atcoder_compile() -> anyhow::Result<()> {
514 let test_dir = tempdir()?;
515
516 let mut file = OpenOptions::new()
518 .write(true)
519 .create(true)
520 .open(format!("{}/Main.cpp", test_dir.path().display()))?;
521 file.write_all(
522 r#"
523#include <iostream>
524using namespace std;
525
526int main() {{
527 return 0;
528}}
529 "#
530 .as_bytes(),
531 )?;
532
533 let contest = Contest::default();
535 let problem = Problem::default();
536 let shell = Shell::default();
537 let compile = ServiceConfig::default_for(ServiceKind::Atcoder).compile;
538 let context = TargetContext::new(ServiceKind::default(), contest.id(), problem.id());
539 let output = shell
540 .exec_templ(&compile, &context)?
541 .current_dir(test_dir.path())
542 .output()
543 .await?;
544 eprintln!("{:?}", output);
545 assert!(output.status.success());
546
547 Ok(())
548 }
549}