1use std::collections::BTreeMap;
2use std::fmt::{Debug, Display};
3use std::io::{Cursor, Read};
4use std::ops::Deref;
5use std::rc::Rc;
6
7use bon::Builder;
8use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
9use cpclib_common::itertools::Itertools;
10use directories::ProjectDirs;
11use flate2::read::GzDecoder;
12use scraper::{Html, Selector};
13use tar::Archive;
14use ureq;
15use ureq::Response;
16use xz2::read::XzDecoder;
17
18use crate::event::EventObserver;
19use crate::runner::runner::{ExternRunner, RunInDir, Runner};
20
21static GITHUB_URL: &str = "https://github.com/";
22
23pub fn cpclib_download(url: &str) -> Result<Box<dyn Read + Send + Sync>, String> {
25 Ok(ureq::get(url)
26 .set("Cache-Control", "max-age=1")
27 .set("From", "krusty.benediction@gmail.com")
28 .set("User-Agent", "cpclib")
29 .call()
30 .map_err(|e| e.to_string())?
31 .into_reader())
32}
33
34pub fn github_get_assets_for_version_url<GI: GithubInformation>(
36 info: &GI
37) -> Result<String, String> {
38 let url = dbg!(format!(
39 "https://github.com/{}/{}/releases",
40 info.owner(),
41 info.project()
42 ));
43
44 let mut content = cpclib_download(&url)?;
46 let mut html = String::new();
47 content
48 .read_to_string(&mut html)
49 .map_err(|e| e.to_string())?;
50 let document = Html::parse_document(&html);
51
52 let selector = Selector::parse("a.Link--primary.Link")
53 .map_err(|e| e.to_string())
54 .map_err(|e| e.to_string())?;
55
56 for link in document.select(&selector) {
59 if let Some(href) = link.attr("href") {
60 let content = link.inner_html();
61 if content.contains(info.version_name()) && href.contains("/tag/") {
62 return Ok(
63 format!("https://github.com{href}").replace("/tag/", "/expanded_assets/")
64 );
65 }
66 }
67 }
68
69 Err(format!("No download link found for {info}"))
70}
71
72#[derive(Default, bon::Builder)]
73#[builder(on(String, into))]
74pub struct MutiplatformUrls {
75 pub linux: Option<String>,
76 pub windows: Option<String>,
77 pub macos: Option<String>
78}
79
80impl MutiplatformUrls {
81 pub fn unique_url(url: &str) -> Self {
82 MutiplatformUrls::builder()
83 .linux(url)
84 .windows(url)
85 .macos(url)
86 .build()
87 }
88
89 pub fn target_os_url(&self) -> Option<&String> {
90 #[cfg(target_os = "windows")]
91 return self.windows.as_ref();
92 #[cfg(target_os = "macos")]
93 return self.macosx.as_ref();
94 #[cfg(target_os = "linux")]
95 return self.linux.as_ref();
96 }
97}
98
99pub trait CompilableInformation {
100 fn target_os_commands(&self) -> Option<&'static [&'static [&'static str]]>;
102
103 fn target_os_compiler<E: EventObserver>(&self) -> Option<Compiler<E>> {
105 if let Some(commands) = self.target_os_commands() {
106 let install: Box<dyn Fn(&Utf8Path, &E) -> Result<(), String>> =
107 Box::new(|_path: &Utf8Path, o: &E| -> Result<(), String> {
108 for command in commands.iter() {
109 ExternRunner::default().inner_run(command, o)?;
110 }
111 Ok(())
112 });
113 let install = Compiler::from(install);
114 Some(install)
115 }
116 else {
117 None
118 }
119 }
120}
121
122pub trait DownloadableInformation {
123 fn target_os_archive_format(&self) -> ArchiveFormat;
124 fn target_os_postinstall<E: EventObserver>(&self) -> Option<PostInstall<E>> {
125 None
126 }
127}
128
129pub trait StaticInformation: DownloadableInformation {
130 fn static_download_urls(&self) -> &'static MutiplatformUrls;
131
132 fn target_os_url(&self) -> Option<&'static str> {
133 self.static_download_urls()
134 .target_os_url()
135 .map(|s| s.as_str())
136 }
137
138 fn target_os_url_generator(&self) -> UrlGenerator {
139 let url = self.target_os_url();
140 let deferred: Box<dyn Fn() -> Result<String, String>> = Box::new(move || {
141 url.ok_or_else(|| "No download url for current OS".to_string())
142 .map(|s| s.to_owned())
143 });
144 deferred.into()
145 }
146}
147
148pub trait ExecutableInformation {
149 fn target_os_folder(&self) -> &'static str;
150 fn target_os_exec_fname(&self) -> &'static str;
151 fn target_os_run_in_dir(&self) -> RunInDir {
152 RunInDir::default()
153 }
154}
155
156pub trait DynamicUrlInformation: DownloadableInformation + Clone + 'static {
157 fn dynamic_download_urls(&self) -> Result<MutiplatformUrls, String>;
158
159 fn target_os_url_generator(&self) -> UrlGenerator {
160 let cloned: Self = self.clone();
161 let deferred: Box<dyn Fn() -> Result<String, String>> =
162 Box::new(move || -> Result<String, String> {
163 let inside: Self = cloned.clone();
164 let urls = inside.dynamic_download_urls()?;
165 urls.target_os_url()
166 .cloned()
167 .ok_or("No url for this os".to_string())
168 });
169 deferred.into()
170 }
171}
172
173pub trait GithubInformation: DownloadableInformation + Display + Clone + 'static {
174 fn project(&self) -> &'static str;
175 fn owner(&self) -> &'static str;
176 fn version_name(&self) -> &'static str;
178 fn linux_key(&self) -> Option<&'static str> {
179 None
180 }
181 fn windows_key(&self) -> Option<&'static str> {
182 None
183 }
184 fn macos_key(&self) -> Option<&'static str> {
185 None
186 }
187
188 fn target_os_url_generator(&self) -> UrlGenerator {
190 let cloned = self.clone();
191 let deferred: Box<dyn Fn() -> Result<String, String>> =
192 Box::new(move || -> Result<String, String> {
193 let inside = cloned.clone();
194 let urls = inside.github_download_urls()?;
195 urls.target_os_url()
196 .cloned()
197 .ok_or("No url for this os".to_string())
198 });
199 deferred.into()
200 }
201
202 fn github_download_urls(&self) -> Result<MutiplatformUrls, String> {
203 let mut content = cpclib_download(&github_get_assets_for_version_url(self)?)?;
204 let mut html = String::default();
205 content
206 .read_to_string(&mut html)
207 .map_err(|e| e.to_string())?;
208 let document = Html::parse_document(&html);
209 let selector = Selector::parse("a")
210 .map_err(|e| e.to_string())
211 .map_err(|e| e.to_string())?;
212
213 let mut map = BTreeMap::new();
214 for element in document.select(&selector) {
215 let name = element.text().collect::<String>();
216 let name = name
217 .replace("\n", "")
218 .replace("\t", " ")
219 .replace(" ", " ");
220 let name = name.trim();
221 map.insert(name.to_owned(), element.attr("href").unwrap().trim());
222 }
223
224 let mut urls = MutiplatformUrls::default();
225
226 if let Some(key) = self.linux_key() {
227 urls.linux = Some(format!(
228 "{}/{}",
229 GITHUB_URL,
230 map.get(key).ok_or_else(|| {
231 format!(
232 "'{}' not found among {}",
233 key,
234 map.keys().map(|s| format!("'{s}'")).join(", ")
235 )
236 })?
237 ));
238 }
239 if let Some(key) = self.windows_key() {
240 urls.windows = Some(format!(
241 "{}/{}",
242 GITHUB_URL,
243 map.get(key).ok_or_else(|| {
244 format!(
245 "'{}' not found among {}",
246 key,
247 map.keys().map(|s| format!("'{s}'")).join(", ")
248 )
249 })?
250 ));
251 }
252 if let Some(key) = self.macos_key() {
253 urls.macos = Some(format!(
254 "{}/{}",
255 GITHUB_URL,
256 map.get(key).ok_or_else(|| {
257 format!(
258 "'{}' not found among {}",
259 key,
260 map.keys().map(|s| format!("'{s}'")).join(", ")
261 )
262 })?
263 ));
264 }
265
266 Ok(urls)
267 }
268}
269
270impl<G> From<&G> for UrlGenerator
271where G: GithubInformation
272{
273 fn from(g: &G) -> Self {
274 g.target_os_url_generator()
275 }
276}
277
278pub trait HasConfiguration {
279 fn configuration<E: EventObserver + 'static>(&self) -> DelegateApplicationDescription<E>;
280}
281
282pub trait GithubCompilableApplication:
283 CompilableInformation + ExecutableInformation + GithubInformation + Default
284{
285 fn configuration<E: EventObserver>(&self) -> DelegateApplicationDescription<E> {
286 DelegateApplicationDescription::builder()
287 .download_fn_url(self) .folder(self.target_os_folder())
289 .archive_format(self.target_os_archive_format())
290 .exec_fname(self.target_os_exec_fname())
291 .maybe_compile(self.target_os_compiler())
292 .in_dir(self.target_os_run_in_dir())
293 .maybe_post_install(self.target_os_postinstall())
294 .build()
295 }
296}
297
298pub trait GithubCompiledApplication: ExecutableInformation + GithubInformation + Default {
299 fn configuration<E: EventObserver>(&self) -> DelegateApplicationDescription<E> {
300 DelegateApplicationDescription::builder()
301 .download_fn_url(self) .folder(self.target_os_folder())
303 .archive_format(self.target_os_archive_format())
304 .exec_fname(self.target_os_exec_fname())
305 .in_dir(self.target_os_run_in_dir())
306 .maybe_post_install(self.target_os_postinstall())
307 .build()
308 }
309}
310
311pub trait InternetStaticCompiledApplication: StaticInformation + ExecutableInformation {
312 fn configuration<E: EventObserver>(&self) -> DelegateApplicationDescription<E> {
313 DelegateApplicationDescription::builder()
314 .download_fn_url(self.target_os_url_generator())
315 .folder(self.target_os_folder())
316 .archive_format(self.target_os_archive_format())
317 .exec_fname(self.target_os_exec_fname())
318 .in_dir(self.target_os_run_in_dir())
319 .maybe_post_install(self.target_os_postinstall())
320 .build()
321 }
322}
323
324pub trait InternetDynamicCompiledApplication:
325 DynamicUrlInformation + ExecutableInformation + Default
326{
327 fn configuration<E: EventObserver>(&self) -> DelegateApplicationDescription<E> {
328 DelegateApplicationDescription::builder()
329 .download_fn_url(self.target_os_url_generator())
330 .folder(self.target_os_folder())
331 .archive_format(self.target_os_archive_format())
332 .exec_fname(self.target_os_exec_fname())
333 .in_dir(self.target_os_run_in_dir())
334 .maybe_post_install(self.target_os_postinstall())
335 .build()
336 }
337}
338
339#[derive(Clone)]
340pub struct UrlGenerator(Rc<Box<dyn Fn() -> Result<String, String>>>);
341
342impl From<Box<dyn Fn() -> String>> for UrlGenerator {
343 fn from(value: Box<dyn Fn() -> String>) -> Self {
344 let wrap = Box::new(move || Ok(value()));
345 Self(Rc::new(wrap))
346 }
347}
348
349impl From<Box<dyn Fn() -> Result<String, String>>> for UrlGenerator {
350 fn from(value: Box<dyn Fn() -> Result<String, String>>) -> Self {
351 Self(Rc::new(value))
352 }
353}
354
355impl From<String> for UrlGenerator {
356 fn from(value: String) -> Self {
357 Self(Rc::new(Box::new(move || Ok(value.clone()))))
358 }
359}
360
361impl From<&str> for UrlGenerator {
362 fn from(value: &str) -> Self {
363 let value = value.to_owned();
364 value.into()
365 }
366}
367
368impl Deref for UrlGenerator {
369 type Target = Box<dyn Fn() -> Result<String, String>>;
370
371 fn deref(&self) -> &Self::Target {
372 &self.0
373 }
374}
375
376#[derive(Clone)]
377pub struct Compiler<E>(Rc<Box<dyn Fn(&Utf8Path, &E) -> Result<(), String>>>);
378impl<E> From<Box<dyn Fn(&Utf8Path, &E) -> Result<(), String>>> for Compiler<E> {
379 fn from(value: Box<dyn Fn(&Utf8Path, &E) -> Result<(), String>>) -> Self {
380 Self(Rc::new(value))
381 }
382}
383
384impl<E> Deref for Compiler<E> {
385 type Target = Box<dyn Fn(&Utf8Path, &E) -> Result<(), String>>;
386
387 fn deref(&self) -> &Self::Target {
388 &self.0
389 }
390}
391
392#[derive(Clone)]
393pub struct PostInstall<E: EventObserver>(
394 Rc<Box<dyn Fn(&DelegateApplicationDescription<E>) -> Result<(), String>>>
395);
396
397impl<E: EventObserver> From<Box<dyn Fn(&DelegateApplicationDescription<E>) -> Result<(), String>>>
398 for PostInstall<E>
399{
400 fn from(value: Box<dyn Fn(&DelegateApplicationDescription<E>) -> Result<(), String>>) -> Self {
401 Self(Rc::new(value))
402 }
403}
404
405impl<E: EventObserver> Deref for PostInstall<E> {
406 type Target = Box<dyn Fn(&DelegateApplicationDescription<E>) -> Result<(), String>>;
407
408 fn deref(&self) -> &Self::Target {
409 &self.0
410 }
411}
412
413#[derive(Clone, Debug)]
414pub enum ArchiveFormat {
415 Raw,
416 Tar,
417 TarGz,
418 TarXz,
419 Zip,
420 SevenZ
421}
422
423#[derive(Builder, Clone)]
424pub struct DelegateApplicationDescription<E: EventObserver> {
425 #[builder(into)]
426 pub download_fn_url: UrlGenerator,
427 pub folder: &'static str,
428 pub exec_fname: &'static str,
429 pub archive_format: ArchiveFormat,
430 #[builder(into)]
431 pub compile: Option<Compiler<E>>,
432 #[builder(into)]
433 pub post_install: Option<PostInstall<E>>,
434 #[builder(default=RunInDir::CurrentDir)]
435 pub in_dir: RunInDir
436}
437
438impl<E: EventObserver> Debug for DelegateApplicationDescription<E> {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 f.debug_struct("DelegateApplicationDescription")
441 .field("folder", &self.folder)
442 .field("exec_fname", &self.exec_fname)
443 .field("archive_format", &self.archive_format)
444 .field("in_dir", &self.in_dir)
445 .finish()
446 }
447}
448
449pub fn base_cache_folder() -> Utf8PathBuf {
450 let proj_dirs = ProjectDirs::from("net.cpcscene", "benediction", "bnd build").unwrap();
451 Utf8Path::from_path(proj_dirs.cache_dir())
452 .unwrap()
453 .to_owned()
454}
455
456pub fn clear_base_cache_folder() -> std::io::Result<()> {
457 std::fs::remove_dir_all(base_cache_folder())
458}
459
460impl<E: EventObserver> DelegateApplicationDescription<E> {
461 pub fn is_cached(&self) -> bool {
462 self.cache_folder().exists()
463 }
464
465 pub fn cache_folder(&self) -> Utf8PathBuf {
466 let base_cache = base_cache_folder();
467
468 if !base_cache.exists() {
469 std::fs::create_dir_all(&base_cache).unwrap();
470 }
471
472 base_cache.join(self.folder).try_into().unwrap()
473 }
474
475 pub fn exec_fname(&self) -> Utf8PathBuf {
476 self.cache_folder().join(self.exec_fname)
477 }
478
479 pub fn install(&self, o: &E) -> Result<(), String> {
480 self.inner_install(o).inspect_err(|e| {
481 dbg!("There was an error, we need to do some cleaning", e);
482 let dest = self.cache_folder();
483 dbg!("Try to remove ", &dest);
484 let _ = std::fs::remove_dir_all(dest); dbg!("Should be done");
486 })
487 }
488
489 fn inner_install(&self, o: &E) -> Result<(), String> {
490 let dest = self.cache_folder();
492
493 let resp = self
494 .download(o)
495 .map_err(|e| format!("Unable to download the expected file. {e}"))?;
496 let mut input = resp.into_reader();
497
498 match self.archive_format {
500 ArchiveFormat::Raw => {
501 o.emit_stdout(&format!(">> Save to {}\n", self.exec_fname()));
502 let mut buffer = Vec::new();
503 input.read_to_end(&mut buffer).unwrap();
504 std::fs::create_dir_all(&dest).map_err(|e| e.to_string())?;
505 std::fs::write(self.exec_fname(), &buffer).map_err(|e| e.to_string())?;
506 },
507 ArchiveFormat::Tar => {
508 o.emit_stdout(">> Open tar archive\n");
509 let mut archive = Archive::new(input);
510 archive.unpack(dest.clone()).map_err(|e| e.to_string())?;
511 },
512 ArchiveFormat::TarGz => {
513 o.emit_stdout(">> Open targz archive\n");
514 let gz = GzDecoder::new(input);
515 let mut archive = Archive::new(gz);
516 archive.unpack(dest.clone()).map_err(|e| e.to_string())?;
517 },
518 ArchiveFormat::TarXz => {
519 o.emit_stdout(">> Open tarxz archive\n");
520 let xz = XzDecoder::new(input);
521 let mut archive = Archive::new(xz);
522 archive.unpack(dest.clone()).map_err(|e| e.to_string())?;
523 },
524 ArchiveFormat::Zip => {
525 o.emit_stdout(">> Unzip archive\n");
526 let mut buffer = Vec::new();
527 input.read_to_end(&mut buffer).unwrap();
528 zip_extract::extract(Cursor::new(buffer), dest.as_std_path(), true)
529 .map_err(|e| e.to_string())?;
530 },
531 ArchiveFormat::SevenZ => {
532 o.emit_stdout(">> Open 7z archive\n");
533 let mut buffer = Vec::new();
534 input.read_to_end(&mut buffer).unwrap();
535 sevenz_rust::decompress(Cursor::new(buffer), dest.as_std_path())
536 .map_err(|e| e.to_string())?;
537 }
538 }
539
540 if let Some(compile) = &self.compile {
541 o.emit_stdout(">> Compile program\n");
542
543 let cwd = std::env::current_dir()
544 .map_err(|e| format!("Unable to get the current working directory {e}.\n"))?;
545 std::env::set_current_dir(&dest)
546 .map_err(|e| format!("Unable to set the current working directory {e}.\n"))?;
547 let res = compile(&dest, o);
548 std::env::set_current_dir(&cwd)
549 .map_err(|e| format!("Unable to set the current working directory {e}.\n"))?;
550 res
551 }
552 else {
553 Ok(())
554 }?;
555
556 if let Some(post_install) = &self.post_install {
557 o.emit_stdout(">> Apply post-installation\n");
558 post_install(self)
559 }
560 else {
561 Ok(())
562 }
563 }
564
565 fn download(&self, o: &E) -> Result<Response, String> {
566 let url = self.download_fn_url.deref()()?;
567 o.emit_stdout(&format!(">> Download file {url}\n"));
568 ureq::get(&url).call().map_err(|e| e.to_string())
569 }
570}
571
572#[derive(Debug)]
573pub struct DelegatedRunner<E: EventObserver> {
574 pub app: DelegateApplicationDescription<E>,
576 pub cmd: String,
578 pub transparent: bool
580}
581
582impl<E: EventObserver> DelegatedRunner<E> {
583 pub fn new(app: DelegateApplicationDescription<E>, cmd: String) -> Self {
584 Self {
585 app,
586 cmd,
587 transparent: false
588 }
589 }
590
591 pub fn new_transparent(app: DelegateApplicationDescription<E>, cmd: String) -> Self {
592 Self {
593 app,
594 cmd,
595 transparent: true
596 }
597 }
598}
599
600impl<E: EventObserver> Runner for DelegatedRunner<E> {
601 type EventObserver = E;
602
603 fn inner_run<S: AsRef<str>>(&self, itr: &[S], o: &E) -> Result<(), String> {
604 let cfg = &self.app;
605
606 if !cfg.is_cached() {
608 o.emit_stdout("> Install application\n");
609 let res = cfg.install(o);
610 if let Err(res) = res {
611 dbg!("Need to leave");
612 return Err(res);
613 }
614 }
615 assert!(cfg.is_cached());
616
617 let mut command = Vec::with_capacity(1 + itr.len());
619 let fname = cfg.exec_fname();
620
621 #[cfg(target_os = "linux")]
622 {
623 if fname.as_str().to_lowercase().ends_with(".exe") {
624 command.push("wine");
625 }
626 }
627
628 command.push(fname.as_str());
629
630 for arg in itr.iter() {
631 command.push(arg.as_ref());
632 }
633
634 let runner = if self.transparent {
636 ExternRunner::<E>::new_transparent(cfg.in_dir)
637 }
638 else {
639 ExternRunner::<E>::new(cfg.in_dir)
640 };
641 runner.inner_run(&command, o)
642 }
643
644 fn get_command(&self) -> &str {
645 &self.cmd
646 }
647}