1#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub struct InputOptions {
76 pub required: bool,
77 pub trim_whitespace: bool,
78}
79
80impl Default for InputOptions {
81 fn default() -> Self {
82 Self {
83 required: false,
84 trim_whitespace: true,
85 }
86 }
87}
88
89#[deprecated]
90pub enum ExitCode {
91 Success = 0,
92 Failure = 1,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
96pub struct AnnotationProperties<'a> {
97 pub title: &'a str,
98 pub file: &'a str,
99 pub start_line: u32,
100 pub end_line: u32,
101 pub start_column: u32,
102 pub end_column: u32,
103}
104
105impl std::fmt::Display for AnnotationProperties<'_> {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let mut properties = Vec::new();
108 if !self.title.is_empty() {
109 properties.push(format!("title={}", encode_command_property(self.title)));
110 }
111 if !self.file.is_empty() {
112 properties.push(format!("file={}", encode_command_property(self.file)));
113 }
114 if self.start_line != 0 {
115 properties.push(format!("line={}", self.start_line));
116 }
117 if self.end_line != 0 {
118 properties.push(format!("endLine={}", self.end_line));
119 }
120 if self.start_column != 0 {
121 properties.push(format!("col={}", self.start_column));
122 }
123 if self.end_column != 0 {
124 properties.push(format!("endCol={}", self.end_column));
125 }
126 write!(f, "{}", properties.join(","))
127 }
128}
129
130fn encode_command_property(property: &str) -> String {
131 property
132 .replace('%', "%25")
133 .replace('\r', "%0D")
134 .replace('\n', "%0A")
135 .replace(':', "%3A")
136 .replace(',', "%2C")
137}
138
139fn encode_command_data(data: &str) -> String {
140 data.replace('%', "%25")
141 .replace('\r', "%0D")
142 .replace('\n', "%0A")
143}
144
145pub fn export_variable(name: impl AsRef<str>, value: impl std::fmt::Display) {
146 let name = name.as_ref();
147 let value = value.to_string();
148 let github_env = std::env::var("GITHUB_ENV").unwrap_or_default();
149 if github_env.is_empty() {
150 println!(
151 "::set-env name={}::{}",
152 encode_command_property(name),
153 encode_command_data(&value)
154 );
155 } else {
156 let mut file = std::fs::OpenOptions::new()
157 .append(true)
158 .open(github_env)
159 .unwrap();
160 let delimiter = uuid::Uuid::new_v4();
161 use std::io::Write;
162 writeln!(file, "{name}<<{delimiter}\n{value}\n{delimiter}").unwrap();
163 }
164}
165
166pub fn set_secret(secret: impl AsRef<str>) {
167 let secret = secret.as_ref();
168 println!("::add-mask::{}", encode_command_data(secret));
169}
170
171pub fn add_path(input_path: impl AsRef<str>) {
172 let input_path = input_path.as_ref();
173 let github_path = std::env::var("GITHUB_PATH").unwrap_or_default();
174 if github_path.is_empty() {
175 println!("::add-path::{}", encode_command_data(input_path));
176 } else {
177 let mut file = std::fs::OpenOptions::new()
178 .append(true)
179 .open(github_path)
180 .unwrap();
181 use std::io::Write;
182 writeln!(file, "{input_path}").unwrap();
183 }
184 let path = std::env::var("PATH").unwrap();
185 const PATH_DELIMITER: &str = if cfg!(windows) { ";" } else { ":" };
186 std::env::set_var("PATH", format!("{input_path}{PATH_DELIMITER}{path}"));
187}
188
189pub fn get_input(name: impl AsRef<str>) -> String {
190 get_input_with_options(name, &InputOptions::default()).unwrap()
191}
192
193pub fn get_input_with_options(
194 name: impl AsRef<str>,
195 options: &InputOptions,
196) -> Result<String, Box<dyn std::error::Error>> {
197 let name = name.as_ref();
198 let name_env = name.replace(' ', "_").to_uppercase();
199 let value = std::env::var(format!("INPUT_{name_env}")).unwrap_or_default();
200 if options.required && value.is_empty() {
201 return Err(format!("{name} is required").into());
202 }
203 if options.trim_whitespace {
204 Ok(value.trim().into())
205 } else {
206 Ok(value)
207 }
208}
209
210pub fn get_multiline_input(name: impl AsRef<str>) -> Vec<String> {
211 get_multiline_input_with_options(name, &InputOptions::default()).unwrap()
212}
213
214pub fn get_multiline_input_with_options(
215 name: impl AsRef<str>,
216 options: &InputOptions,
217) -> Result<Vec<String>, Box<dyn std::error::Error>> {
218 let value = get_input_with_options(name, options)?;
219 let lines: Vec<String> = value
220 .lines()
221 .filter_map(|x| if x.is_empty() { None } else { Some(x.into()) })
222 .collect();
223 if options.trim_whitespace {
224 Ok(lines.into_iter().map(|x| x.trim().into()).collect())
225 } else {
226 Ok(lines)
227 }
228}
229
230pub fn get_boolean_input(name: impl AsRef<str>) -> bool {
231 get_boolean_input_with_options(name, &InputOptions::default()).unwrap()
232}
233
234pub fn get_boolean_input_with_options(
235 name: impl AsRef<str>,
236 options: &InputOptions,
237) -> Result<bool, Box<dyn std::error::Error>> {
238 let name = name.as_ref();
239 let value = get_input_with_options(name, options)?;
240 const TRUE_VALUES: &[&str] = &["true", "True", "TRUE"];
241 const FALSE_VALUES: &[&str] = &["false", "False", "FALSE"];
242 if TRUE_VALUES.contains(&value.as_str()) {
243 Ok(true)
244 } else if FALSE_VALUES.contains(&value.as_str()) {
245 Ok(false)
246 } else {
247 Err(format!("{name} is not a valid boolean").into())
248 }
249}
250
251pub fn set_output(name: impl AsRef<str>, value: impl std::fmt::Display) {
252 let name = name.as_ref();
253 let value = value.to_string();
254 let github_output = std::env::var("GITHUB_OUTPUT").unwrap_or_default();
255 if github_output.is_empty() {
256 println!(
257 "::set-output name={}::{}",
258 encode_command_property(name),
259 encode_command_data(&value)
260 );
261 } else {
262 let mut file = std::fs::OpenOptions::new()
263 .append(true)
264 .open(github_output)
265 .unwrap();
266 let delimiter = uuid::Uuid::new_v4();
267 use std::io::Write;
268 writeln!(file, "{name}<<{delimiter}\n{value}\n{delimiter}").unwrap();
269 }
270}
271
272pub fn set_command_echo(enabled: bool) {
273 println!("::echo::{}", if enabled { "on" } else { "off" });
274}
275
276pub fn set_failed(message: impl std::fmt::Display) -> ! {
277 error(message);
278 panic!();
279}
280
281pub fn is_debug() -> bool {
282 std::env::var("RUNNER_DEBUG").is_ok_and(|x| x == "1")
283}
284
285pub fn debug(message: impl std::fmt::Display) {
286 debug_with_properties(message, &AnnotationProperties::default());
287}
288
289pub fn debug_with_properties(message: impl std::fmt::Display, properties: &AnnotationProperties) {
290 let message = message.to_string();
291 println!("::debug {}::{}", properties, encode_command_data(&message));
292}
293
294pub fn error(message: impl std::fmt::Display) {
295 error_with_properties(message, &AnnotationProperties::default());
296}
297
298pub fn error_with_properties(message: impl std::fmt::Display, properties: &AnnotationProperties) {
299 let message = message.to_string();
300 println!("::error {}::{}", properties, encode_command_data(&message));
301}
302
303pub fn warning(message: impl std::fmt::Display) {
304 warning_with_properties(message, &AnnotationProperties::default());
305}
306
307pub fn warning_with_properties(message: impl std::fmt::Display, properties: &AnnotationProperties) {
308 let message = message.to_string();
309 println!(
310 "::warning {}::{}",
311 properties,
312 encode_command_data(&message)
313 );
314}
315
316pub fn notice(message: impl std::fmt::Display) {
317 notice_with_properties(message, &AnnotationProperties::default());
318}
319
320pub fn notice_with_properties(message: impl std::fmt::Display, properties: &AnnotationProperties) {
321 let message = message.to_string();
322 println!("::notice {}::{}", properties, encode_command_data(&message));
323}
324
325pub fn info(message: impl std::fmt::Display) {
326 println!("{message}");
327}
328
329pub fn start_group(name: impl AsRef<str>) {
330 let name = name.as_ref();
331 println!("::group::{}", encode_command_data(name));
332}
333
334pub fn end_group() {
335 println!("::endgroup::");
336}
337
338pub fn group<T, F: FnOnce() -> T>(name: impl AsRef<str>, f: F) -> T {
339 struct GroupResource;
341 impl Drop for GroupResource {
342 fn drop(&mut self) {
343 end_group();
344 }
345 }
346 start_group(name);
347 let _group = GroupResource {};
348 f()
349}
350
351pub fn save_state(name: impl AsRef<str>, value: impl std::fmt::Display) {
352 let name = name.as_ref();
353 let value = value.to_string();
354 let github_state = std::env::var("GITHUB_STATE").unwrap_or_default();
355 if github_state.is_empty() {
356 println!(
357 "::save-state name={}::{}",
358 encode_command_property(name),
359 encode_command_data(&value)
360 );
361 } else {
362 let mut file = std::fs::OpenOptions::new()
363 .append(true)
364 .open(github_state)
365 .unwrap();
366 let delimiter = uuid::Uuid::new_v4();
367 use std::io::Write;
368 writeln!(file, "{name}<<{delimiter}\n{value}\n{delimiter}").unwrap();
369 }
370}
371
372pub fn get_state(name: impl AsRef<str>) -> String {
373 let name = name.as_ref();
374 std::env::var(format!("STATE_{name}")).unwrap_or_default()
375}
376
377pub fn get_id_token() -> Result<String, Box<dyn std::error::Error>> {
378 get_id_token_with_audience("")
379}
380
381pub fn get_id_token_with_audience(
382 audience: impl AsRef<str>,
383) -> Result<String, Box<dyn std::error::Error>> {
384 #[derive(serde::Deserialize)]
385 struct TokenResponse {
386 value: String,
387 }
388 let audience = audience.as_ref();
389 let mut url = std::env::var("ACTIONS_ID_TOKEN_REQUEST_URL")?;
390 if !audience.is_empty() {
391 url.push_str(&format!("&audience={audience}"));
392 }
393 let response = reqwest::blocking::get(url)?;
394 let json: TokenResponse = response.json()?;
395 let id_token = json.value;
396 set_secret(&id_token);
397 Ok(id_token)
398}
399
400pub fn to_posix_path(path: &str) -> String {
401 path.replace('\\', "/")
402}
403
404pub fn to_win32_path(path: &str) -> String {
405 path.replace('/', "\\")
406}
407
408pub fn to_platform_path(path: &str) -> String {
409 if cfg!(windows) {
410 to_win32_path(path)
411 } else {
412 to_posix_path(path)
413 }
414}
415
416pub const SUMMARY_ENV_VAR: &str = "GITHUB_STEP_SUMMARY";
417pub const SUMMARY_DOCS_URL: &str = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
418
419#[derive(Debug, Clone, PartialEq, Eq)]
420pub enum SummaryTableRowItem<'a> {
421 SummaryTableCell(SummaryTableCell<'a>),
422 String(String),
423}
424
425pub type SummaryTableRow<'a> = &'a [SummaryTableRowItem<'a>];
426
427#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct SummaryTableCell<'a> {
429 pub data: &'a str,
430 pub header: bool,
431 pub colspan: &'a str,
432 pub rowspan: &'a str,
433}
434
435impl Default for SummaryTableCell<'_> {
436 fn default() -> Self {
437 Self {
438 data: "",
439 header: false,
440 colspan: "1",
441 rowspan: "1",
442 }
443 }
444}
445
446#[derive(Default, Debug, Clone, PartialEq, Eq)]
447pub struct SummaryImageOptions<'a> {
448 pub width: &'a str,
449 pub height: &'a str,
450}
451
452#[derive(Default, Debug, Clone, PartialEq, Eq)]
453pub struct SummaryWriteOptions {
454 pub overwrite: bool,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct Summary {
459 buffer: String,
460 path: String,
461}
462
463impl Default for Summary {
464 fn default() -> Self {
465 Self::new()
466 }
467}
468
469mod dom {
470 #[derive(Debug, Clone, PartialEq, Eq)]
471 pub struct HtmlElement {
472 pub tag_name: String,
473 pub attributes: std::collections::HashMap<String, String>,
474 pub child_nodes: Vec<Node>,
475 }
476
477 impl HtmlElement {
478 pub fn new(tag_name: impl AsRef<str>) -> Self {
479 Self {
480 tag_name: tag_name.as_ref().to_string(),
481 attributes: std::collections::HashMap::new(),
482 child_nodes: Vec::new(),
483 }
484 }
485
486 pub fn with_children(tag_name: impl AsRef<str>, child_nodes: impl AsRef<[Node]>) -> Self {
487 Self {
488 tag_name: tag_name.as_ref().to_string(),
489 attributes: std::collections::HashMap::new(),
490 child_nodes: child_nodes.as_ref().to_vec(),
491 }
492 }
493 }
494
495 impl std::fmt::Display for HtmlElement {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497 const VOID_ELEMENTS: &[&str] = &[
498 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
499 "param", "source", "track", "wbr",
500 ];
501 write!(f, "<{}", self.tag_name)?;
502 for (key, value) in &self.attributes {
503 write!(f, " {}=\"{}\"", key, value)?;
504 }
505 if VOID_ELEMENTS.contains(&self.tag_name.as_str()) {
506 write!(f, " />")?;
507 } else {
508 write!(f, ">")?;
509 for child_node in &self.child_nodes {
510 write!(f, "{}", child_node)?;
511 }
512 write!(f, "</{}>", self.tag_name)?;
513 }
514 Ok(())
515 }
516 }
517
518 #[derive(Debug, Clone, PartialEq, Eq)]
519 pub enum Node {
520 String(String),
521 HtmlElement(HtmlElement),
522 }
523
524 impl std::fmt::Display for Node {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 match self {
527 Node::String(string) => write!(f, "{}", string),
528 Node::HtmlElement(element) => write!(f, "{}", element),
529 }
530 }
531 }
532}
533
534impl Summary {
535 pub fn new() -> Self {
536 let path = std::env::var("GITHUB_STEP_SUMMARY").unwrap();
537 Self {
538 buffer: String::new(),
539 path,
540 }
541 }
542
543 pub fn write(&mut self) -> Result<&mut Self, Box<dyn std::error::Error>> {
544 self.write_with_options(&SummaryWriteOptions::default())
545 }
546
547 pub fn write_with_options(
548 &mut self,
549 options: &SummaryWriteOptions,
550 ) -> Result<&mut Self, Box<dyn std::error::Error>> {
551 if options.overwrite {
552 std::fs::write(&self.path, &self.buffer)?;
553 } else {
554 let mut file = std::fs::OpenOptions::new().append(true).open(&self.path)?;
555 use std::io::Write;
556 write!(file, "{}", self.buffer)?;
557 }
558 self.buffer = String::new();
559 Ok(self)
560 }
561
562 pub fn clear(&mut self) -> Result<&mut Self, Box<dyn std::error::Error>> {
563 self.buffer = String::new();
564 self.write()?;
565 Ok(self)
566 }
567
568 pub fn stringify(&self) -> String {
569 self.buffer.clone()
570 }
571
572 pub fn is_empty_buffer(&self) -> bool {
573 self.buffer.is_empty()
574 }
575
576 pub fn empty_buffer(&mut self) -> &mut Self {
577 self.buffer = String::new();
578 self
579 }
580
581 pub fn add_raw(&mut self, text: impl AsRef<str>) -> &mut Self {
582 self.add_raw_with_add_eol(text, false);
583 self
584 }
585
586 pub fn add_raw_with_add_eol(&mut self, text: impl AsRef<str>, add_eol: bool) -> &mut Self {
587 let text = text.as_ref();
588 self.buffer.push_str(text);
589 if add_eol {
590 self.buffer.push('\n');
591 }
592 self
593 }
594
595 pub fn add_eol(&mut self) -> &mut Self {
596 self.buffer.push('\n');
597 self
598 }
599
600 pub fn add_code_block(&mut self, code: impl AsRef<str>) -> &mut Self {
601 self.add_code_block_with_lang(code, "");
602 self
603 }
604
605 pub fn add_code_block_with_lang(
606 &mut self,
607 code: impl AsRef<str>,
608 lang: impl AsRef<str>,
609 ) -> &mut Self {
610 let code = code.as_ref();
611 let lang = lang.as_ref();
612 self.buffer.push_str(&format!("```{lang}\n{code}\n```"));
613 self.buffer.push('\n');
614 self
615 }
616
617 pub fn add_list(&mut self, items: &[impl AsRef<str>]) -> &mut Self {
618 self.add_list_with_ordered(items, false);
619 self
620 }
621
622 pub fn add_list_with_ordered(&mut self, items: &[impl AsRef<str>], ordered: bool) -> &mut Self {
623 let mut ul_or_ol = if ordered {
624 dom::HtmlElement::new("ol")
625 } else {
626 dom::HtmlElement::new("ul")
627 };
628 for item in items {
629 let item = item.as_ref().to_string();
630 let li = dom::HtmlElement::with_children("li", &[dom::Node::String(item)]);
631 ul_or_ol.child_nodes.push(dom::Node::HtmlElement(li));
632 }
633 self.buffer.push_str(&ul_or_ol.to_string());
634 self.buffer.push('\n');
635 self
636 }
637
638 pub fn add_table<'a>(&mut self, rows: impl AsRef<[SummaryTableRow<'a>]>) -> &mut Self {
639 let mut table = dom::HtmlElement::new("table");
640 for row in rows.as_ref() {
641 let mut tr = dom::HtmlElement::new("tr");
642 for item in row.iter() {
643 match item {
644 SummaryTableRowItem::String(string) => {
645 let td = dom::HtmlElement::with_children(
646 "td",
647 &[dom::Node::String(string.clone())],
648 );
649 tr.child_nodes.push(dom::Node::HtmlElement(td));
650 }
651 SummaryTableRowItem::SummaryTableCell(cell) => {
652 let mut th_or_td = if cell.header {
653 dom::HtmlElement::new("th")
654 } else {
655 dom::HtmlElement::new("td")
656 };
657 if !cell.colspan.is_empty() {
658 th_or_td
659 .attributes
660 .insert("colspan".into(), cell.colspan.to_string());
661 }
662 if !cell.rowspan.is_empty() {
663 th_or_td
664 .attributes
665 .insert("rowspan".into(), cell.rowspan.to_string());
666 }
667 th_or_td
668 .child_nodes
669 .push(dom::Node::String(cell.data.to_string()));
670 tr.child_nodes.push(dom::Node::HtmlElement(th_or_td));
671 }
672 }
673 }
674 table.child_nodes.push(dom::Node::HtmlElement(tr));
675 }
676 self.buffer.push_str(&table.to_string());
677 self.buffer.push('\n');
678 self
679 }
680
681 pub fn add_details(&mut self, label: impl AsRef<str>, content: impl AsRef<str>) -> &mut Self {
682 let label = label.as_ref();
683 let content = content.as_ref();
684 let mut details = dom::HtmlElement::new("details");
685 let summary = dom::HtmlElement::with_children("summary", [dom::Node::String(label.into())]);
686 details.child_nodes.push(dom::Node::HtmlElement(summary));
687 details.child_nodes.push(dom::Node::String(content.into()));
688 self.buffer.push_str(&details.to_string());
689 self.buffer.push('\n');
690 self
691 }
692
693 pub fn add_image(&mut self, src: impl AsRef<str>, alt: impl AsRef<str>) -> &mut Self {
694 self.add_image_with_options(src, alt, &SummaryImageOptions::default());
695 self
696 }
697
698 pub fn add_image_with_options(
699 &mut self,
700 src: impl AsRef<str>,
701 alt: impl AsRef<str>,
702 options: &SummaryImageOptions,
703 ) -> &mut Self {
704 let src = src.as_ref();
705 let alt = alt.as_ref();
706 let mut img = dom::HtmlElement::new("img");
707 img.attributes.insert("src".into(), src.into());
708 img.attributes.insert("alt".into(), alt.into());
709 if !options.width.is_empty() {
710 img.attributes.insert("width".into(), options.width.into());
711 }
712 if !options.height.is_empty() {
713 img.attributes
714 .insert("height".into(), options.height.into());
715 }
716 self.buffer.push_str(&img.to_string());
717 self.buffer.push('\n');
718 self
719 }
720
721 pub fn add_heading(&mut self, text: impl AsRef<str>) -> &mut Self {
722 self.add_heading_with_level(text, 1);
723 self
724 }
725
726 pub fn add_heading_with_level(&mut self, text: impl AsRef<str>, level: u8) -> &mut Self {
727 let text = text.as_ref();
728 let level = if [1, 2, 3, 4, 5, 6].contains(&level) {
729 level
730 } else {
731 1
732 };
733 let mut h = dom::HtmlElement::new(format!("h{}", level));
734 h.child_nodes.push(dom::Node::String(text.into()));
735 self.buffer.push_str(&h.to_string());
736 self.buffer.push('\n');
737 self
738 }
739
740 pub fn add_separator(&mut self) -> &mut Self {
741 self.buffer.push_str("<hr />");
742 self.buffer.push('\n');
743 self
744 }
745
746 pub fn add_break(&mut self) -> &mut Self {
747 self.buffer.push_str("<br />");
748 self.buffer.push('\n');
749 self
750 }
751
752 pub fn add_quote(&mut self, text: impl AsRef<str>) -> &mut Self {
753 self.add_quote_with_cite(text, "");
754 self
755 }
756
757 pub fn add_quote_with_cite(
758 &mut self,
759 text: impl AsRef<str>,
760 cite: impl AsRef<str>,
761 ) -> &mut Self {
762 let text = text.as_ref();
763 let cite = cite.as_ref();
764 let mut blockquote = dom::HtmlElement::new("blockquote");
765 if !cite.is_empty() {
766 blockquote.attributes.insert("cite".into(), cite.into());
767 }
768 blockquote.child_nodes.push(dom::Node::String(text.into()));
769 self.buffer.push_str(&blockquote.to_string());
770 self.buffer.push('\n');
771 self
772 }
773
774 pub fn add_link(&mut self, text: impl AsRef<str>, href: impl AsRef<str>) -> &mut Self {
775 let text = text.as_ref();
776 let href = href.as_ref();
777 let mut a = dom::HtmlElement::new("a");
778 a.attributes.insert("href".into(), href.into());
779 a.child_nodes.push(dom::Node::String(text.into()));
780 self.buffer.push_str(&a.to_string());
781 self.buffer.push('\n');
782 self
783 }
784}
785
786lazy_static::lazy_static! {
787 static ref SUMMARY: Summary = Summary::new();
788}
789lazy_static::lazy_static! {
790 static ref MARKDOWN_SUMMARY: &'static Summary = &*SUMMARY;
792}
793
794pub mod platform {
795 #[cfg(target_os = "windows")]
796 fn get_windows_info() -> Result<(String, String), Box<dyn std::error::Error>> {
797 let version = Command::new("powershell")
798 .arg("-command")
799 .arg("(Get-CimInstance -ClassName Win32_OperatingSystem).Version")
800 .output()?
801 .stdout;
802 let version = String::from_utf8(version)?;
803 let name = Command::new("powershell")
804 .arg("-command")
805 .arg("(Get-CimInstance -ClassName Win32_OperatingSystem).Caption")
806 .output()?
807 .stdout;
808 let name = String::from_utf8(name)?;
809 Ok((name.trim().to_string(), version.trim().to_string()))
810 }
811
812 #[cfg(target_os = "macos")]
813 fn get_macos_info() -> Result<(String, String), Box<dyn std::error::Error>> {
814 let version = Command::new("sw_vers")
815 .arg("-productVersion")
816 .output()?
817 .stdout;
818 let version = String::from_utf8(version)?;
819 let name = Command::new("sw_vers").arg("-productName").output()?.stdout;
820 let name = String::from_utf8(name)?;
821 Ok((name.trim().to_string(), version.trim().to_string()))
822 }
823
824 #[cfg(target_os = "linux")]
825 fn get_linux_info() -> Result<(String, String), Box<dyn std::error::Error>> {
826 let name = std::process::Command::new("lsb_release")
827 .arg("-is")
828 .output()?
829 .stdout;
830 let name = String::from_utf8(name)?;
831 let version = std::process::Command::new("lsb_release")
832 .arg("-rs")
833 .output()?
834 .stdout;
835 let version = String::from_utf8(version)?;
836 Ok((name.trim().to_string(), version.trim().to_string()))
837 }
838
839 #[cfg(target_os = "windows")]
840 pub const PLATFORM: &str = "win32";
841 #[cfg(target_os = "macos")]
842 pub const PLATFORM: &str = "darwin";
843 #[cfg(target_os = "linux")]
844 pub const PLATFORM: &str = "linux";
845 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
846 compile_error!("unsupported target_os");
847
848 #[cfg(target_arch = "x86_64")]
849 pub const ARCH: &str = "x86_64";
850 #[cfg(target_arch = "x86")]
851 pub const ARCH: &str = "x86";
852 #[cfg(target_arch = "aarch64")]
853 pub const ARCH: &str = "arm64";
854 #[cfg(target_arch = "arm")]
855 pub const ARCH: &str = "arm";
856 #[cfg(not(any(
857 target_arch = "x86_64",
858 target_arch = "x86",
859 target_arch = "aarch64",
860 target_arch = "arm"
861 )))]
862 compile_error!("unsupported target_arch");
863
864 pub const IS_WINDOWS: bool = cfg!(target_os = "windows");
865 pub const IS_MACOS: bool = cfg!(target_os = "macos");
866 pub const IS_LINUX: bool = cfg!(target_os = "linux");
867
868 pub struct Details {
869 pub name: String,
870 pub platform: String,
871 pub arch: String,
872 pub version: String,
873 pub is_windows: bool,
874 pub is_macos: bool,
875 pub is_linux: bool,
876 }
877
878 pub fn get_details() -> Result<Details, Box<dyn std::error::Error>> {
879 #[cfg(target_os = "windows")]
880 let (name, version) = get_windows_info()?;
881 #[cfg(target_os = "macos")]
882 let (name, version) = get_macos_info()?;
883 #[cfg(target_os = "linux")]
884 let (name, version) = get_linux_info()?;
885 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
886 compile_error!("unsupported target_os");
887 Ok(Details {
888 name,
889 platform: PLATFORM.to_string(),
890 arch: ARCH.to_string(),
891 version,
892 is_windows: IS_WINDOWS,
893 is_macos: IS_MACOS,
894 is_linux: IS_LINUX,
895 })
896 }
897}