assemble_core/defaults/tasks/
wrapper.rs1use 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#[derive(Debug)]
33pub struct WrapperTask {
34 pub wrapper_name: Prop<String>,
36 pub assemble_url: Prop<Url>,
38 pub assemble_version: Prop<String>,
40 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#[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 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#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
279pub struct Distribution {
280 pub url: Url,
282 pub os: Os,
284}
285
286#[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}