assemble_core/defaults/tasks/
wrapper.rs

1//! The wrapper task allows for creating a wrapper for assemble that should never fail
2
3use crate::__export::TaskId;
4use crate::cryptography::Sha256;
5use crate::defaults::tasks::wrapper::github::GetDistribution;
6use crate::exception::BuildException;
7use crate::lazy_evaluation::{Prop, Provider, ProviderExt};
8use crate::project::error::ProjectError;
9use crate::project::error::ProjectResult;
10use crate::task::create_task::CreateTask;
11use crate::task::flags::{OptionDeclarationBuilder, OptionDeclarations, OptionsDecoder};
12use crate::task::initialize_task::InitializeTask;
13use crate::task::task_io::TaskIO;
14use crate::task::up_to_date::UpToDate;
15
16use crate::{cryptography, BuildResult, Executable, Project, Task, ASSEMBLE_HOME};
17
18use std::fs::File;
19use std::io;
20use std::io::Read;
21use std::io::Write as _;
22use std::path::{Path, PathBuf};
23use strum_macros::{Display, EnumIter};
24
25use toml_edit::{value, Document};
26use url::Url;
27use crate::error::PayloadError;
28
29mod github;
30
31/// Create assemble wrapper files
32#[derive(Debug)]
33pub struct WrapperTask {
34    /// The base name of the generate wrapper file. Appended with .bat for batch file variant
35    pub wrapper_name: Prop<String>,
36    /// The url of the specified assemble distributable
37    pub assemble_url: Prop<Url>,
38    /// if a direct url isn't provided, download from default provider with given version
39    pub assemble_version: Prop<String>,
40    /// If provided, compare the downloaded file with a string representing it's sha256 value. Fails
41    /// if downloaded file doesn't match
42    pub assemble_sha256: Prop<Sha256>,
43}
44
45impl Executable<WrapperTask> {
46    fn shell_script_location(&self) -> impl Provider<PathBuf> {
47        let workspace = self.project().with(|p| p.root_dir());
48        self.wrapper_name
49            .clone()
50            .map(move |name| workspace.join(name))
51    }
52
53    fn bat_script_location(&self) -> impl Provider<PathBuf> {
54        let workspace = self.project().with(|p| p.root_dir());
55        self.wrapper_name
56            .clone()
57            .map(move |name| workspace.join(format!("{}.bat", name)))
58    }
59
60    fn get_release_url(&self) -> Result<Url, ProjectError> {
61        let distribution = github::get_distributions(&self.assemble_version.get())?.get_relevant();
62        self.assemble_url
63            .try_get()
64            .or_else(|| distribution.map(|d| d.url))
65            .ok_or_else(|| ProjectError::custom("No distribution could be determined"))
66    }
67}
68
69impl UpToDate for WrapperTask {}
70
71impl CreateTask for WrapperTask {
72    fn new(using_id: &TaskId, _project: &Project) -> ProjectResult<Self> {
73        Ok(Self {
74            wrapper_name: using_id.prop("name").map_err(PayloadError::new)?,
75            assemble_url: using_id.prop("url").map_err(PayloadError::new)?,
76            assemble_version: using_id.prop("version").map_err(PayloadError::new)?,
77            assemble_sha256: using_id.prop("sha256").map_err(PayloadError::new)?,
78        })
79    }
80
81    fn description() -> String {
82        "Creates wrapper files for running assemble without requiring assemble to be already installed".to_string()
83    }
84
85    fn options_declarations() -> Option<OptionDeclarations> {
86        Some(OptionDeclarations::new::<Self, _>([
87            OptionDeclarationBuilder::<String>::new("version")
88                .use_from_str()
89                .optional(true)
90                .build(),
91            OptionDeclarationBuilder::<String>::new("url")
92                .use_from_str()
93                .optional(true)
94                .build(),
95        ]))
96    }
97
98    fn try_set_from_decoder(&mut self, decoder: &OptionsDecoder) -> ProjectResult<()> {
99        if let Some(version) = decoder.get_value::<String>("version").map_err(PayloadError::new)? {
100            self.assemble_version.set(version).map_err(PayloadError::new)?;
101        }
102        if let Some(url) = decoder.get_value::<String>("url").map_err(PayloadError::new)? {
103            let url = Url::parse(&url).map_err(ProjectError::custom)?;
104            self.assemble_url.set(url).map_err(PayloadError::new)?;
105        }
106
107        Ok(())
108    }
109}
110
111impl InitializeTask for WrapperTask {
112    fn initialize(task: &mut Executable<Self>, _project: &Project) -> ProjectResult {
113        let default_version = env!("CARGO_PKG_VERSION");
114        task.assemble_version.set(default_version).map_err(PayloadError::new)?;
115        task.wrapper_name.set("assemble").map_err(PayloadError::new)?;
116
117        Ok(())
118    }
119}
120
121impl TaskIO for WrapperTask {
122    fn configure_io(task: &mut Executable<Self>) -> ProjectResult {
123        let shell_script = task.shell_script_location();
124        let bat_script = task.bat_script_location();
125        task.work().add_output_provider(shell_script);
126        task.work().add_output_provider(bat_script);
127        Ok(())
128    }
129}
130
131impl Task for WrapperTask {
132    fn task_action(task: &mut Executable<Self>, project: &Project) -> BuildResult {
133        let wrapper_properties_path = project
134            .root_dir()
135            .join("assemble")
136            .join("wrapper")
137            .join("assemble.toml");
138
139        let mut settings: Document = {
140            let mut file = File::open(&wrapper_properties_path)?;
141            let mut toml = Vec::new();
142            file.read_to_end(&mut toml)?;
143            let as_string = String::from_utf8(toml).map_err(PayloadError::<BuildException>::new)?;
144            as_string.parse().map_err(BuildException::new)?
145        };
146
147        let mut updated_url = false;
148        let distribution_url = task.assemble_url.fallible_get().map_err(PayloadError::<BuildException>::new)?;
149        if settings["url"].to_string() != distribution_url.to_string() {
150            settings["url"] = value(distribution_url.to_string());
151            updated_url = true;
152        }
153
154        let wrapper_settings = toml_edit::de::from_document::<WrapperSettings>(settings.clone()).map_err(PayloadError::<BuildException>::new)?;
155
156        if let Some(distribution_info) = wrapper_settings.existing_distribution() {
157            if distribution_info.is_valid() && !updated_url {
158                task.work().set_did_work(false);
159                return Err(BuildException::StopTask.into());
160            }
161        }
162
163        info!("settings = {:#?}", settings);
164
165        let _shell_file = task.shell_script_location().fallible_get().map_err(PayloadError::<BuildException>::new)?;
166        let _bat_file = task.bat_script_location().fallible_get().map_err(PayloadError::<BuildException>::new)?;
167
168        {
169            let mut file = File::create(&wrapper_properties_path).map_err(PayloadError::<BuildException>::new)?;
170            writeln!(file, "{}", settings).map_err(PayloadError::<BuildException>::new)?;
171        }
172
173        Ok(())
174    }
175}
176
177fn generate_shell_script(_dest_file: &Path) -> Result<(), BuildResult> {
178    Ok(())
179}
180
181fn generate_bat_script(_dest_file: &Path) -> Result<(), BuildResult> {
182    Ok(())
183}
184
185#[derive(Debug, Deserialize, Serialize)]
186struct WrapperSettings {
187    url: Url,
188    sha256: Option<Sha256>,
189    dist_base: String,
190    store_base: Option<String>,
191    dist_path: String,
192    store_path: Option<String>,
193}
194
195impl WrapperSettings {
196    fn dist_path(&self) -> PathBuf {
197        let path = self
198            .dist_base
199            .replace("ASSEMBLE_HOME", &*ASSEMBLE_HOME.path().to_string_lossy());
200        println!("replaced = {path:?}");
201        Path::new(&path).join(&self.dist_path.trim_start_matches('/'))
202    }
203
204    fn store_path(&self) -> PathBuf {
205        let path = self
206            .store_base
207            .as_ref()
208            .unwrap_or(&self.dist_base)
209            .replace("ASSEMBLE_HOME", &*ASSEMBLE_HOME.path().to_string_lossy());
210        println!("replaced = {path:?}");
211        Path::new(&path)
212            .join(
213                self.store_path
214                    .as_ref()
215                    .unwrap_or(&self.dist_path)
216                    .trim_start_matches('/'),
217            )
218            .join(
219                PathBuf::from(self.url.path())
220                    .file_name()
221                    .expect("no file path"),
222            )
223    }
224
225    fn config_path(&self) -> PathBuf {
226        self.dist_path().join("config.json")
227    }
228
229    fn existing_distribution(&self) -> Option<DistributionInfo> {
230        let path = self.config_path();
231        let file = File::open(path).ok()?;
232
233        serde_json::from_reader(file).ok()
234    }
235
236    fn save_distribution_info(&self, info: DistributionInfo) -> io::Result<()> {
237        let path = self.config_path();
238        let file = File::create(path)?;
239
240        serde_json::to_writer_pretty(file, &info)?;
241        Ok(())
242    }
243}
244
245/// Downloaded distribution info.
246#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
247pub struct DistributionInfo {
248    distribution: Distribution,
249    executable_path: PathBuf,
250    sha256: Sha256,
251}
252
253impl DistributionInfo {
254    pub fn executable_path(&self) -> &PathBuf {
255        &self.executable_path
256    }
257
258    /// Check whether the distribution is valid
259    pub fn is_valid(&self) -> bool {
260        if !self.executable_path.exists() {
261            return false;
262        }
263        let metadata = self
264            .executable_path
265            .metadata()
266            .expect("could not get metadata");
267        if !metadata.is_file() {
268            return false;
269        }
270
271        let sha = cryptography::hash_file_sha256(&self.executable_path)
272            .expect("could not get sha256 of file");
273        sha != self.sha256
274    }
275}
276
277/// A distribution of assemble
278#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
279pub struct Distribution {
280    /// The url the distribution can be downloaded from
281    pub url: Url,
282    /// The os this distribution supports
283    pub os: Os,
284}
285
286/// The os of a host system
287#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, EnumIter, Display, Serialize, Deserialize)]
288#[strum(serialize_all = "lowercase")]
289pub enum Os {
290    #[cfg_attr(target_os = "macos", default)]
291    MacOs,
292    #[cfg_attr(target_os = "windows", default)]
293    Windows,
294    #[cfg_attr(target_os = "linux", default)]
295    Linux,
296}
297
298#[cfg(test)]
299mod tests {
300    use crate::defaults::tasks::wrapper::WrapperSettings;
301
302    use toml::toml;
303
304    #[test]
305    fn get_distribution_info_from_settings() {
306        let settings = toml! {
307            url = "https://github.com/joshradin/assemble-rs/releases/download/v0.1.2/assemble-linux-amd64"
308            dist_base = "ASSEMBLE_HOME"
309            dist_path = "/wrapper/dists"
310
311        }.try_into::<WrapperSettings>().unwrap();
312
313        println!("dist_base = {:?}", settings.dist_base);
314        println!("dist_path = {:?}", settings.dist_path());
315        println!("store_path = {:?}", settings.store_path());
316    }
317}