gemfile_downloader/
lib.rs

1use std::error::Error;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use futures::future::join_all;
5use serde::{Deserialize, Serialize};
6use tokio::fs::read_to_string;
7use tokio::sync::Mutex;
8use crate::parser::GemfileData;
9
10pub mod parser;
11pub mod download;
12pub mod unpack_gem;
13pub mod unpack_tar_gz;
14pub mod gem_version;
15
16///
17/// インストール結果の情報
18///
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct InstallInfo {
21    // インストールしたGemの一覧
22    pub install_gems: Vec<String>,
23    // Gemfileが含まれていた場合、すべてのGem名とGemfileのパス
24    pub find_gemfiles: Vec<FindGemFileInfo>,
25}
26
27///
28/// インストール時に見つかったGemfileの情報
29///
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct FindGemFileInfo {
32    // Gemの名前
33    pub gem_name: String,
34    // Gemfileのパス
35    pub gemfile_path: PathBuf,
36}
37
38///
39/// Gemfileを読み込み、Gemのインストールを行う
40///
41/// * gemfile - Gemfileのパス
42/// * install_dictionary - Gemのインストール先のディレクトリ
43/// * cache_directory - Gemのダウンロード先のキャッシュディレクトリ
44///
45/// return -  インストール処理の結果
46///
47pub async fn install_from_gemfile_file(gemfile: &Path, install_dictionary: &Path, cache_directory: &Path) -> Result<InstallInfo, Box<dyn Error>> {
48    // Gemfileの内容を取得
49    let gemfile_context = read_to_string(gemfile).await?;
50
51    // Gemのダウンロード
52    install_from_gemfile_literal(&gemfile_context, install_dictionary, cache_directory).await
53}
54
55///
56/// Gemfileの文字列のデータから、Gemのインストールを行う
57///
58/// * gemfile_context - Gemfileの内容
59/// * install_dictionary - Gemのインストール先のディレクトリ
60/// * cache_directory - Gemのダウンロード先のキャッシュディレクトリ
61///
62/// return - インストール処理の結果
63///
64pub async fn install_from_gemfile_literal(gemfile_context: &str, install_dictionary: &Path, cache_directory: &Path) -> Result<InstallInfo, Box<dyn Error>> {
65    // パース
66    let gemfile_data = parser::GemfileData::parse(gemfile_context).await?;
67
68    install_gems(gemfile_data, install_dictionary, cache_directory).await
69}
70
71///
72/// Gemのインストールを行う
73///
74/// * gemfile_data - Gemfileの読み込み済みデータ
75/// * install_dictionary - Gemのインストール先のディレクトリ
76/// * cache_directory - Gemのダウンロード先のキャッシュディレクトリ
77///
78/// return - インストール処理の結果
79///
80pub async fn install_gems(gemfile_data: GemfileData, install_dictionary: &Path, cache_directory: &Path) -> Result<InstallInfo, Box<dyn Error>>{
81
82    // インストールしたGemの一覧
83    let installed_gems: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
84    // インストールしたGemに含まれていたGemfileのパス
85    let gemfiles: Arc<Mutex<Vec<FindGemFileInfo>>> = Arc::new(Mutex::new(Vec::new()));
86
87    // gemをすべてダウンロード
88    let tasks: Vec<_> = gemfile_data.gems.into_iter().map(|gem| {
89        let installed_gems = Arc::clone(&installed_gems);
90        let gemfiles = Arc::clone(&gemfiles);
91        let source = gemfile_data.source.clone();
92
93        async move {
94            // ダウンロード
95            let download_result = download::download_gem(cache_directory, &source, &gem).await;
96            let Ok(download_result) = download_result else {
97                return;
98            };
99            let gem_name = download_result.file_stem();
100            let Some(gem_name) = gem_name else {
101                return;
102            };
103
104            // キャッシュディレクトリ
105            let cache_directory =  &cache_directory.join(gem_name);
106            // gemの本体を置くディレクトリ
107            let gems_directory = &install_dictionary.join(gem_name);
108
109            // .gemを解凍
110            let gz_result = unpack_gem::unpack_gem(&download_result, cache_directory);
111            let Ok(gz_result) = gz_result else {
112                return;
113            };
114
115            // .tar.gzを解凍
116            let tar_gz_result = unpack_tar_gz::unpack_tar_gz(&gz_result, cache_directory, gems_directory);
117            let Ok(tar_gz_result) = tar_gz_result else {
118                return;
119            };
120
121            let gem_name = gem_name.to_string_lossy().to_string();
122            // インストール一覧に追加
123            installed_gems.lock().await.push(gem_name.clone());
124
125            // gemfileのパスを追加
126            if let Some(gemfile) = tar_gz_result {
127                gemfiles.lock().await.push(FindGemFileInfo{
128                    gem_name,
129                    gemfile_path: gemfile,
130                });
131            }
132        }
133    }).collect();
134    join_all(tasks).await;
135
136    // Arcを外す
137    let Ok(installed_gems) = Arc::try_unwrap(installed_gems) else {
138        return Err("installed_gems unwrap error".into());
139    };
140    let Ok(gemfiles) = Arc::try_unwrap(gemfiles) else {
141        return Err("gemfiles unwrap error".into());
142    };
143
144    Ok(InstallInfo{
145        install_gems: installed_gems.into_inner(),
146        find_gemfiles: gemfiles.into_inner(),
147    })
148}
149
150#[cfg(test)]
151mod tests {
152    use std::path::Path;
153    use crate::install_from_gemfile_literal;
154
155    ///
156    /// Gemsのダウンロードのテスト
157    ///
158    #[tokio::test]
159    pub async fn gems_download_test() {
160        // gemのダウンロードを置くキャッシュ
161        let gems_cache_directory = Path::new("./target/gems_cache");
162        // gemを最終的に解凍するディレクトリ
163        let gems_directory = Path::new("./target/gems");
164
165        // ファイルの内容
166        let gemfile = "
167source \"https://rubygems.org\"
168
169gemspec
170
171group :development, :test do
172 gem \"docile\", \"~> 1.4.0\"
173 gem \"simplecov-html\", \"~> 0.12.3\"
174 gem \"i18n\", \"~> 1.8.5\"
175 gem \"concurrent-ruby\", \"~> 1.3.4\"
176end";
177        let result = install_from_gemfile_literal(gemfile, gems_directory, gems_cache_directory).await;
178        assert!(result.is_ok());
179
180        result.unwrap().find_gemfiles.iter().for_each(|find_gemfile| {
181            println!("gemfile: {:?}", find_gemfile.gemfile_path);
182        });
183    }
184}