Skip to main content

lindera_core/
assets.rs

1use std::error::Error;
2use std::path::Path;
3
4use crate::dictionary_builder::DictionaryBuilder;
5
6pub struct FetchParams {
7    /// Dictionary file name
8    pub file_name: &'static str,
9
10    /// MeCab directory
11    pub input_dir: &'static str,
12
13    /// Lindera directory
14    pub output_dir: &'static str,
15
16    /// Dummy input for docs.rs
17    pub dummy_input: &'static str,
18
19    /// URL from which to fetch the asset
20    pub download_url: &'static str,
21}
22
23#[cfg(not(target_os = "windows"))]
24fn empty_directory(dir: &Path) -> Result<(), Box<dyn Error>> {
25    if dir.is_dir() {
26        for entry in std::fs::read_dir(dir)? {
27            let entry = entry?;
28            let path = entry.path();
29            if path.is_dir() {
30                std::fs::remove_dir_all(&path)?;
31            } else {
32                std::fs::remove_file(&path)?;
33            }
34        }
35    }
36    Ok(())
37}
38
39#[cfg(target_os = "windows")]
40fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), Box<dyn Error>> {
41    if !dst.exists() {
42        std::fs::create_dir(dst)?;
43    }
44
45    for entry in std::fs::read_dir(src)? {
46        let entry = entry?;
47        let entry_path = entry.path();
48        let dst_path = dst.join(entry.file_name());
49
50        if entry_path.is_dir() {
51            copy_dir_all(&entry_path, &dst_path)?;
52        } else {
53            std::fs::copy(&entry_path, &dst_path)?;
54        }
55    }
56    Ok(())
57}
58
59/// Fetch the necessary assets and then build the dictionary using `builder`
60pub fn fetch(params: FetchParams, builder: impl DictionaryBuilder) -> Result<(), Box<dyn Error>> {
61    use std::env;
62    use std::fs::{create_dir, rename, File};
63    use std::io::{self, Cursor, Read, Write};
64    use std::path::{Path, PathBuf};
65
66    use encoding::all::UTF_8;
67    use encoding::{EncoderTrap, Encoding};
68    use flate2::read::GzDecoder;
69    use tar::Archive;
70
71    println!("cargo:rerun-if-changed=build.rs");
72    println!("cargo:rerun-if-changed=Cargo.toml");
73
74    // Directory path for build package
75    // if the `LINDERA_CACHE` variable is defined, behaves like a cache, where data is invalidated only:
76    // - on new lindera-assets version
77    // - if the LINDERA_CACHE dir changed
78    // otherwise, keeps behavior of always redownloading and rebuilding
79    let (build_dir, is_cache) = if let Some(lindera_cache_dir) = env::var_os("LINDERA_CACHE") {
80        (
81            PathBuf::from(lindera_cache_dir).join(env::var_os("CARGO_PKG_VERSION").unwrap()),
82            true,
83        )
84    } else {
85        (
86            PathBuf::from(env::var_os("OUT_DIR").unwrap()), /* ex) target/debug/build/<pkg>/out */
87            false,
88        )
89    };
90
91    // environment variable passed to dependents, that will actually be used to include the dictionary in the library
92    println!("cargo::rustc-env=LINDERA_WORKDIR={}", build_dir.display());
93
94    std::fs::create_dir_all(&build_dir)?;
95
96    let input_dir = build_dir.join(params.input_dir);
97
98    let output_dir = build_dir.join(params.output_dir);
99
100    // Fast path where the data is already in cache
101    if is_cache && output_dir.is_dir() {
102        return Ok(());
103    }
104
105    if std::env::var("DOCS_RS").is_ok() {
106        // Create directory for dummy input directory for build docs
107        create_dir(&input_dir)?;
108
109        // Create dummy char.def
110        let mut dummy_char_def = File::create(input_dir.join("char.def"))?;
111        dummy_char_def.write_all(b"DEFAULT 0 1 0\n")?;
112
113        // Create dummy CSV file
114        let mut dummy_dict_csv = File::create(input_dir.join("dummy_dict.csv"))?;
115        dummy_dict_csv.write_all(
116            &UTF_8
117                .encode(params.dummy_input, EncoderTrap::Ignore)
118                .unwrap(),
119        )?;
120
121        // Create dummy unk.def
122        File::create(input_dir.join("unk.def"))?;
123        let mut dummy_matrix_def = File::create(input_dir.join("matrix.def"))?;
124        dummy_matrix_def.write_all(b"0 1 0\n")?;
125    } else {
126        // Source file path for build package
127        let source_path_for_build = &build_dir.join(params.file_name);
128
129        // Download source file to build directory
130        let tmp_path = Path::new(&build_dir).join(params.file_name.to_owned() + ".download");
131
132        // Download a tarball
133        let resp = ureq::get(params.download_url).call()?;
134        let mut dest = File::create(&tmp_path)?;
135
136        io::copy(&mut resp.into_reader(), &mut dest)?;
137        dest.flush()?;
138
139        rename(tmp_path.clone(), source_path_for_build).expect("Failed to rename temporary file");
140
141        // Decompress a tar.gz file
142        let tmp_extract_path =
143            Path::new(&build_dir).join(format!("tmp-archive-{}", params.input_dir));
144        let tmp_extracted_path = tmp_extract_path.join(params.input_dir);
145        let _ = std::fs::remove_dir_all(&tmp_extract_path);
146        std::fs::create_dir_all(&tmp_extract_path)?;
147
148        let mut tar_gz = File::open(source_path_for_build)?;
149        let mut buffer = Vec::new();
150        tar_gz.read_to_end(&mut buffer)?;
151        let cursor = Cursor::new(buffer);
152        let decoder = GzDecoder::new(cursor);
153        let mut archive = Archive::new(decoder);
154        archive.unpack(&tmp_extract_path)?;
155
156        #[cfg(target_os = "windows")]
157        {
158            // Recreate input_dir to avoid conflicts when copying the directory on Windows systems (which do not support overwriting directories).
159            // Check if output_dir exists
160            if input_dir.exists() {
161                // Remove input_dir
162                std::fs::remove_dir_all(&input_dir).expect("Failed to remove input directory");
163
164                // Make input_dir
165                std::fs::create_dir_all(&input_dir).expect("Failed to create input directory");
166            }
167
168            // Copy tmp_path to input_dir
169            copy_dir_all(&tmp_extracted_path, &input_dir)
170                .expect("Failed to copy files from temporary directory to input directory");
171
172            // remove tmp_path
173            std::fs::remove_dir_all(&tmp_extracted_path)
174                .expect("Failed to remove temporary directory");
175        }
176        #[cfg(not(target_os = "windows"))]
177        {
178            // Empty the input directory first to avoid conflicts when renaming the directory later on Linux and macOS systems (which do not support overwriting directories).
179            empty_directory(&input_dir).expect("Failed to empty input directory");
180            rename(tmp_extracted_path, &input_dir).expect("Failed to rename archive directory");
181        }
182
183        let _ = std::fs::remove_dir_all(&tmp_extract_path);
184        drop(dest);
185        let _ = std::fs::remove_file(source_path_for_build);
186    }
187
188    let tmp_path = build_dir.join(format!("tmp-output-{}", params.output_dir));
189    let _ = std::fs::remove_dir_all(&tmp_path);
190
191    builder.build_dictionary(&input_dir, &tmp_path)?;
192
193    #[cfg(target_os = "windows")]
194    {
195        // Check if output_dir exists
196        if output_dir.exists() {
197            // Remove output_dir
198            std::fs::remove_dir_all(&output_dir).expect("Failed to remove output directory");
199
200            // Make output_dir
201            std::fs::create_dir_all(&output_dir).expect("Failed to create output directory");
202        }
203
204        // Copy tmp_path to output_dir
205        copy_dir_all(&tmp_path, &output_dir).expect("Failed to copy output directory");
206
207        // remove tmp_path
208        std::fs::remove_dir_all(&tmp_path).expect("Failed to copy output directory");
209    }
210
211    #[cfg(not(target_os = "windows"))]
212    {
213        // Empty the output directory
214        empty_directory(&output_dir).expect("Failed to empty output directory");
215
216        // Rename tmp_path to output_dir
217        rename(tmp_path, &output_dir).expect("Failed to rename output directory");
218    }
219
220    let _ = std::fs::remove_dir_all(&input_dir);
221
222    Ok(())
223}