Skip to main content

unlab_gpu/pkg/
github.rs

1//
2// Copyright (c) 2026 Ɓukasz Szpakowski
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7//
8//! A module of [GitHub](https://github.com) source.
9use std::str;
10use crate::serde_json;
11use super::*;
12
13/// A service domain.
14pub const SERVICE_DOMAIN: &'static str = "github.com";
15/// A service API domain.
16pub const SERVICE_API_DOMAIN: &'static str = "api.github.com";
17
18#[derive(Clone, Debug, Deserialize)]
19struct Ref
20{
21    #[serde(rename = "ref")]
22    ref1: String,
23}
24
25/// A structure of [GitHub](https://github.com) source.
26#[derive(Clone)]
27pub struct GitHubSrc
28{
29    name: PkgName,
30    old_name: Option<PkgName>,
31    home_dir: PathBuf,
32    work_dir: PathBuf,
33    printer: Arc<dyn Print + Send + Sync>,
34    versions: Option<BTreeSet<Version>>,
35    current_version: Option<Version>,
36    dir: Option<PathBuf>,
37}
38
39impl GitHubSrc
40{
41    /// Creates a [GitHub](https://github.com) source.
42    pub fn new(name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Self>
43    {
44        let original_name = old_name.as_ref().unwrap_or(&name);
45        if original_name.name().split('/').count() != 3 {
46            return None;
47        }
48        match original_name.name().split_once('/') {
49            Some((domain, _)) if domain == SERVICE_DOMAIN => {
50                Some(GitHubSrc {
51                        name,
52                        old_name,
53                        home_dir,
54                        work_dir,
55                        printer,
56                        versions: None,
57                        current_version: None,
58                        dir: None,
59                })
60            },
61            _ => None,
62        }
63    }
64    
65    /// Returns the package name.
66    pub fn name(&self) -> &PkgName
67    { &self.name }
68
69    /// Returns the old package name if the source has the old package name, otherwise `None`.
70    pub fn old_name(&self) -> Option<&PkgName>
71    { 
72        match &self.old_name {
73            Some(old_name) => Some(old_name),
74            None => None,
75        }
76    }
77
78    /// Returns the path to the Unlab-gpu home directory.
79    pub fn home_dir(&self) -> &Path
80    { self.home_dir.as_path() }
81
82    /// Returns the path to the work directory of current package.
83    pub fn work_dir(&self) -> &Path
84    { self.work_dir.as_path() }
85
86    /// Returns the printer.
87    pub fn printer(&self) -> &Arc<dyn Print + Send + Sync>
88    { &self.printer }
89
90    /// Returns the current package version.
91    pub fn current_version(&self) -> Option<&Version>
92    { 
93        match &self.current_version {
94            Some(current_version) => Some(current_version),
95            None => None,
96        }
97    }
98
99    fn update_versions(&self, is_update: bool) -> Result<BTreeSet<Version>>
100    {
101        let original_name = self.old_name.as_ref().unwrap_or(&self.name);
102        let repo_path = match original_name.name().split_once('/') {
103            Some((_, tmp_repo_path)) => tmp_repo_path,
104            None => return Err(Error::PkgName(self.name.clone(), String::from("no package repository path"))),
105        };
106        update_pkg_versions(&self.name, &self.old_name, self.home_dir.as_path(), is_update, &self.printer, || {
107                let mut easy = curl::easy::Easy::new();
108                easy.url(format!("https://{}/repos/{}/git/matching-refs/tags", SERVICE_API_DOMAIN, str_to_url_name(repo_path, true)).as_str())?;
109                let mut http_headers = List::new();
110                http_headers.append(USER_AGENT_HTTP_HEADER)?;
111                http_headers.append("Accept: application/vnd.github+json")?;
112                http_headers.append("X-GitHub-Api-Version: 2022-11-28")?;
113                easy.http_headers(http_headers)?;
114                easy.follow_location(true)?;
115                Ok(easy)
116        }, |data| {
117                let s = match str::from_utf8(data) {
118                    Ok(tmp_s) => tmp_s,
119                    Err(_) => return Err(Error::PkgName(self.name.clone(), String::from("data contains invalid UTF-8 character"))),
120                };
121                let refs: Vec<Ref> = match serde_json::from_str(s) {
122                    Ok(tmp_refs) => tmp_refs,
123                    Err(err) => return Err(Error::SerdeJson(err)),
124                };
125                let mut versions: BTreeSet<Version> = BTreeSet::new();
126                for ref1 in &refs {
127                    let tag_ref_prefix = "refs/tags/";
128                    if ref1.ref1.starts_with(tag_ref_prefix) {
129                        let tag_name = &ref1.ref1[tag_ref_prefix.len()..];
130                        match tag_name_to_version(tag_name) {
131                            Some(version) => {
132                                versions.insert(version);
133                            },
134                            None => (),
135                        }
136                    }
137                }
138                Ok(versions)
139        })
140    }
141}
142
143impl Source for GitHubSrc
144{
145    fn update(&mut self) -> Result<()>
146    {
147        self.versions = Some(self.update_versions(true)?);
148        Ok(())
149    }
150    
151    fn versions(&mut self) -> Result<&BTreeSet<Version>>
152    {
153        if self.versions.is_none() {
154            self.versions = Some(self.update_versions(false)?);
155        }
156        match &self.versions {
157            Some(versions) => Ok(versions),
158            None => return Err(Error::PkgName(self.name.clone(), String::from("no package versions"))),
159        }
160    }
161    
162    fn set_current_version(&mut self, version: Version)
163    { self.current_version = Some(version); }
164    
165    fn dir(&mut self) -> Result<&Path>
166    {
167        if self.dir.is_none() {
168            match &self.current_version {
169                Some(current_version) => {
170                    self.dir = Some(extract_pkg_file(&self.name, current_version, &self.work_dir, &self.printer, || {
171                            let original_name = self.old_name.as_ref().unwrap_or(&self.name); 
172                            let tag_name = version_to_tag_name(current_version);
173                            let url = format!("https://{}/archive/refs/tags/{}.tar.gz", str_to_url_name(original_name.name(), true), str_to_url_name(tag_name.as_str(), false));
174                            download_pkg_file(&self.name, &self.old_name, current_version, url.as_str(), &self.home_dir, &self.printer)
175                    })?)
176                },
177                None => return Err(Error::PkgName(self.name.clone(), String::from("no current package version"))),
178            }
179        }
180        match &self.dir {
181            Some(versions) => Ok(versions),
182            None => return Err(Error::PkgName(self.name.clone(), String::from("no package directory"))),
183        }
184    }
185}
186
187/// A structure of factory of [GitHub](https://github.com) source.
188#[derive(Copy, Clone, Debug)]
189pub struct GitHubSrcFactory;
190
191impl GitHubSrcFactory
192{
193    /// Creates a factory of [GitHub](https://github.com) source.
194    pub fn new() -> Self
195    { GitHubSrcFactory }
196}
197
198impl SourceCreate for GitHubSrcFactory
199{
200    fn create(&self, name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Box<dyn Source + Send + Sync>>
201    { 
202        match GitHubSrc::new(name, old_name, home_dir, work_dir, printer) {
203            Some(src) => Some(Box::new(src)),
204            None => None,
205        }
206    }
207}