diff --git a/Cargo.toml b/Cargo.toml index b07a2b6c5..c8e30778b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["tools/codegen"] +members = ["tools/codegen", "tools/manifest-schema"] # This table is shared by projects under github.com/taiki-e. # It is not intended for manual editing. diff --git a/tools/codegen/Cargo.toml b/tools/codegen/Cargo.toml index 9888ea94f..1b0bb8c0a 100644 --- a/tools/codegen/Cargo.toml +++ b/tools/codegen/Cargo.toml @@ -4,6 +4,7 @@ edition = "2021" default-run = "generate-manifest" [dependencies] +taiki-e-install-action-manifest-schema = { version = "0.1.0", path = "../manifest-schema" } anyhow = "1" flate2 = "1" fs-err = "2" diff --git a/tools/codegen/src/lib.rs b/tools/codegen/src/lib.rs index 2ec5ece22..325f8e95b 100644 --- a/tools/codegen/src/lib.rs +++ b/tools/codegen/src/lib.rs @@ -2,21 +2,9 @@ #![allow(clippy::missing_panics_doc, clippy::too_long_first_doc_paragraph)] -use std::{ - cmp::{self, Reverse}, - collections::BTreeMap, - env, fmt, - path::{Path, PathBuf}, - slice, - str::FromStr, -}; +use std::{env, path::PathBuf}; -use anyhow::Result; -use serde::{ - de::{self, Deserialize, Deserializer}, - ser::{Serialize, Serializer}, -}; -use serde_derive::{Deserialize, Serialize}; +pub use taiki_e_install_action_manifest_schema::*; #[must_use] pub fn workspace_root() -> PathBuf { @@ -25,366 +13,3 @@ pub fn workspace_root() -> PathBuf { dir.pop(); // tools dir } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Version { - pub major: Option, - pub minor: Option, - pub patch: Option, - pub pre: semver::Prerelease, - pub build: semver::BuildMetadata, -} - -impl Version { - #[must_use] - pub fn omitted(major: u64, minor: Option) -> Self { - Self { - major: Some(major), - minor, - patch: None, - pre: semver::Prerelease::default(), - build: semver::BuildMetadata::default(), - } - } - #[must_use] - pub fn latest() -> Self { - Self { - major: None, - minor: None, - patch: None, - pre: semver::Prerelease::default(), - build: semver::BuildMetadata::default(), - } - } - #[must_use] - pub fn to_semver(&self) -> Option { - Some(semver::Version { - major: self.major?, - minor: self.minor?, - patch: self.patch?, - pre: self.pre.clone(), - build: self.build.clone(), - }) - } -} -impl From for Version { - fn from(v: semver::Version) -> Self { - Self { - major: Some(v.major), - minor: Some(v.minor), - patch: Some(v.patch), - pre: v.pre, - build: v.build, - } - } -} -impl PartialOrd for Version { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Ord for Version { - fn cmp(&self, other: &Self) -> cmp::Ordering { - pub(crate) fn convert(v: &Version) -> semver::Version { - semver::Version { - major: v.major.unwrap_or(u64::MAX), - minor: v.minor.unwrap_or(u64::MAX), - patch: v.patch.unwrap_or(u64::MAX), - pre: v.pre.clone(), - build: v.build.clone(), - } - } - convert(self).cmp(&convert(other)) - } -} -impl fmt::Display for Version { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Some(major) = self.major else { - f.write_str("latest")?; - return Ok(()); - }; - f.write_str(&major.to_string())?; - let Some(minor) = self.minor else { - return Ok(()); - }; - f.write_str(".")?; - f.write_str(&minor.to_string())?; - let Some(patch) = self.patch else { - return Ok(()); - }; - f.write_str(".")?; - f.write_str(&patch.to_string())?; - if !self.pre.is_empty() { - f.write_str("-")?; - f.write_str(&self.pre)?; - } - if !self.build.is_empty() { - f.write_str("+")?; - f.write_str(&self.build)?; - } - Ok(()) - } -} -impl FromStr for Version { - type Err = semver::Error; - fn from_str(s: &str) -> Result { - if s == "latest" { - return Ok(Self::latest()); - } - match s.parse::() { - Ok(v) => Ok(v.into()), - Err(e) => match s.parse::() { - Ok(v) => Ok(Self { - major: Some(v.major), - minor: v.minor, - patch: v.patch, - pre: semver::Prerelease::default(), - build: semver::BuildMetadata::default(), - }), - Err(_e) => Err(e), - }, - } - } -} -impl Serialize for Version { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - String::serialize(&self.to_string(), serializer) - } -} -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - String::deserialize(deserializer)?.parse().map_err(de::Error::custom) - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Manifests { - pub rust_crate: Option, - pub template: Option, - /// Markdown for the licenses. - pub license_markdown: String, - #[serde(flatten)] - pub map: BTreeMap, ManifestRef>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ManifestRef { - Ref { version: Version }, - Real(Manifest), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Manifest { - #[serde(skip_serializing_if = "Option::is_none")] - pub previous_stable_version: Option, - #[serde(flatten)] - pub download_info: BTreeMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ManifestDownloadInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, - pub etag: String, - pub checksum: String, - /// Path to binaries in archive. Default to `${tool}${exe}`. - #[serde(skip_serializing_if = "Option::is_none")] - pub bin: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ManifestTemplate { - #[serde(flatten)] - pub download_info: BTreeMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ManifestTemplateDownloadInfo { - pub url: String, - /// Path to binaries in archive. Default to `${tool}${exe}`. - #[serde(skip_serializing_if = "Option::is_none")] - pub bin: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct BaseManifest { - /// Link to the GitHub repository. - pub repository: String, - /// Alternative link for the project. Automatically detected if possible. - pub website: Option, - /// Markdown syntax for links to licenses. Automatically detected if possible. - pub license_markdown: Option, - /// Prefix of release tag. - pub tag_prefix: String, - /// Crate name, if this is Rust crate. - pub rust_crate: Option, - pub default_major_version: Option, - /// Asset name patterns. - pub asset_name: Option, - /// Path to binaries in archive. Default to `${tool}${exe}`. - pub bin: Option, - pub signing: Option, - #[serde(default)] - pub broken: Vec, - pub version_range: Option, - /// Use glibc build if host_env is gnu. - #[serde(default)] - pub prefer_linux_gnu: bool, - /// Check that the version is yanked not only when updating the manifest, - /// but also when running the action. - #[serde(default)] - pub immediate_yank_reflection: bool, - pub platform: BTreeMap, -} -impl BaseManifest { - /// Validate the manifest. - pub fn validate(&self) { - for bin in self.bin.iter().chain(self.platform.values().flat_map(|m| &m.bin)) { - assert!(!bin.as_slice().is_empty()); - for bin in bin.as_slice() { - let file_name = Path::new(bin).file_name().unwrap().to_str().unwrap(); - if !self.repository.ends_with("/xbuild") { - assert!( - !(file_name.contains("${version") || file_name.contains("${rust")), - "{bin}" - ); - } - } - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Signing { - pub kind: SigningKind, -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -#[serde(deny_unknown_fields)] -pub enum SigningKind { - /// algorithm: minisign - /// public key: package.metadata.binstall.signing.pubkey at Cargo.toml - /// - MinisignBinstall, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct BaseManifestPlatformInfo { - /// Asset name patterns. Default to the value at `BaseManifest::asset_name`. - pub asset_name: Option, - /// Path to binaries in archive. Default to the value at `BaseManifest::bin`. - pub bin: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum StringOrArray { - String(String), - Array(Vec), -} - -impl StringOrArray { - #[must_use] - pub fn as_slice(&self) -> &[String] { - match self { - Self::String(s) => slice::from_ref(s), - Self::Array(v) => v, - } - } - #[must_use] - pub fn map(&self, mut f: impl FnMut(&String) -> String) -> Self { - match self { - Self::String(s) => Self::String(f(s)), - Self::Array(v) => Self::Array(v.iter().map(f).collect()), - } - } -} - -/// GitHub Actions Runner supports Linux (x86_64, aarch64, arm), Windows (x86_64, aarch64), -/// and macOS (x86_64, aarch64). -/// https://github.com/actions/runner/blob/v2.315.0/.github/workflows/build.yml#L21 -/// https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners -/// -/// Note: -/// - Static-linked binaries compiled for linux-musl will also work on linux-gnu systems and are -/// usually preferred over linux-gnu binaries because they can avoid glibc version issues. -/// (rustc enables statically linking for linux-musl by default, except for mips.) -/// - Binaries compiled for x86_64 macOS will usually also work on aarch64 macOS. -/// - Binaries compiled for x86_64 Windows will usually also work on aarch64 Windows 11+. -/// - Ignore arm for now, as we need to consider the version and whether hard-float is supported. -/// https://github.com/rust-lang/rustup/pull/593 -/// https://github.com/cross-rs/cross/pull/1018 -/// Does it seem only armv7l+ is supported? -/// https://github.com/actions/runner/blob/v2.315.0/src/Misc/externals.sh#L189 -/// https://github.com/actions/runner/issues/688 -// TODO: support musl with dynamic linking like wasmtime 22.0.0+'s musl binaries: https://github.com/bytecodealliance/wasmtime/releases/tag/v22.0.0 -#[allow(non_camel_case_types)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum HostPlatform { - x86_64_linux_gnu, - x86_64_linux_musl, - x86_64_macos, - x86_64_windows, - aarch64_linux_gnu, - aarch64_linux_musl, - aarch64_macos, - aarch64_windows, -} - -impl HostPlatform { - #[must_use] - pub fn rust_target(self) -> &'static str { - match self { - Self::x86_64_linux_gnu => "x86_64-unknown-linux-gnu", - Self::x86_64_linux_musl => "x86_64-unknown-linux-musl", - Self::x86_64_macos => "x86_64-apple-darwin", - Self::x86_64_windows => "x86_64-pc-windows-msvc", - Self::aarch64_linux_gnu => "aarch64-unknown-linux-gnu", - Self::aarch64_linux_musl => "aarch64-unknown-linux-musl", - Self::aarch64_macos => "aarch64-apple-darwin", - Self::aarch64_windows => "aarch64-pc-windows-msvc", - } - } - #[must_use] - pub fn rust_target_arch(self) -> &'static str { - match self { - Self::aarch64_linux_gnu - | Self::aarch64_linux_musl - | Self::aarch64_macos - | Self::aarch64_windows => "aarch64", - Self::x86_64_linux_gnu - | Self::x86_64_linux_musl - | Self::x86_64_macos - | Self::x86_64_windows => "x86_64", - } - } - #[must_use] - pub fn rust_target_os(self) -> &'static str { - match self { - Self::aarch64_linux_gnu - | Self::aarch64_linux_musl - | Self::x86_64_linux_gnu - | Self::x86_64_linux_musl => "linux", - Self::aarch64_macos | Self::x86_64_macos => "macos", - Self::aarch64_windows | Self::x86_64_windows => "windows", - } - } - #[must_use] - pub fn exe_suffix(self) -> &'static str { - match self { - Self::x86_64_windows | Self::aarch64_windows => ".exe", - _ => "", - } - } -} diff --git a/tools/manifest-schema/Cargo.toml b/tools/manifest-schema/Cargo.toml new file mode 100644 index 000000000..a8255c70f --- /dev/null +++ b/tools/manifest-schema/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "taiki-e-install-action-manifest-schema" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +semver = { version = "1", features = ["serde"] } +serde = "1" +serde_derive = "1" + +[lints] +workspace = true diff --git a/tools/manifest-schema/src/lib.rs b/tools/manifest-schema/src/lib.rs new file mode 100644 index 000000000..fe096bdfc --- /dev/null +++ b/tools/manifest-schema/src/lib.rs @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![allow(clippy::missing_panics_doc, clippy::too_long_first_doc_paragraph)] + +use std::{ + cmp::{self, Reverse}, + collections::BTreeMap, + fmt, + path::Path, + slice, + str::FromStr, +}; + +use anyhow::Result; +use serde::{ + de::{self, Deserialize, Deserializer}, + ser::{Serialize, Serializer}, +}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Version { + pub major: Option, + pub minor: Option, + pub patch: Option, + pub pre: semver::Prerelease, + pub build: semver::BuildMetadata, +} + +impl Version { + #[must_use] + pub fn omitted(major: u64, minor: Option) -> Self { + Self { + major: Some(major), + minor, + patch: None, + pre: semver::Prerelease::default(), + build: semver::BuildMetadata::default(), + } + } + #[must_use] + pub fn latest() -> Self { + Self { + major: None, + minor: None, + patch: None, + pre: semver::Prerelease::default(), + build: semver::BuildMetadata::default(), + } + } + #[must_use] + pub fn to_semver(&self) -> Option { + Some(semver::Version { + major: self.major?, + minor: self.minor?, + patch: self.patch?, + pre: self.pre.clone(), + build: self.build.clone(), + }) + } +} +impl From for Version { + fn from(v: semver::Version) -> Self { + Self { + major: Some(v.major), + minor: Some(v.minor), + patch: Some(v.patch), + pre: v.pre, + build: v.build, + } + } +} +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Version { + fn cmp(&self, other: &Self) -> cmp::Ordering { + pub(crate) fn convert(v: &Version) -> semver::Version { + semver::Version { + major: v.major.unwrap_or(u64::MAX), + minor: v.minor.unwrap_or(u64::MAX), + patch: v.patch.unwrap_or(u64::MAX), + pre: v.pre.clone(), + build: v.build.clone(), + } + } + convert(self).cmp(&convert(other)) + } +} +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Some(major) = self.major else { + f.write_str("latest")?; + return Ok(()); + }; + f.write_str(&major.to_string())?; + let Some(minor) = self.minor else { + return Ok(()); + }; + f.write_str(".")?; + f.write_str(&minor.to_string())?; + let Some(patch) = self.patch else { + return Ok(()); + }; + f.write_str(".")?; + f.write_str(&patch.to_string())?; + if !self.pre.is_empty() { + f.write_str("-")?; + f.write_str(&self.pre)?; + } + if !self.build.is_empty() { + f.write_str("+")?; + f.write_str(&self.build)?; + } + Ok(()) + } +} +impl FromStr for Version { + type Err = semver::Error; + fn from_str(s: &str) -> Result { + if s == "latest" { + return Ok(Self::latest()); + } + match s.parse::() { + Ok(v) => Ok(v.into()), + Err(e) => match s.parse::() { + Ok(v) => Ok(Self { + major: Some(v.major), + minor: v.minor, + patch: v.patch, + pre: semver::Prerelease::default(), + build: semver::BuildMetadata::default(), + }), + Err(_e) => Err(e), + }, + } + } +} +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + String::serialize(&self.to_string(), serializer) + } +} +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)?.parse().map_err(de::Error::custom) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Manifests { + pub rust_crate: Option, + pub template: Option, + /// Markdown for the licenses. + pub license_markdown: String, + #[serde(flatten)] + pub map: BTreeMap, ManifestRef>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ManifestRef { + Ref { version: Version }, + Real(Manifest), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_stable_version: Option, + #[serde(flatten)] + pub download_info: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManifestDownloadInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + pub etag: String, + pub checksum: String, + /// Path to binaries in archive. Default to `${tool}${exe}`. + #[serde(skip_serializing_if = "Option::is_none")] + pub bin: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManifestTemplate { + #[serde(flatten)] + pub download_info: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ManifestTemplateDownloadInfo { + pub url: String, + /// Path to binaries in archive. Default to `${tool}${exe}`. + #[serde(skip_serializing_if = "Option::is_none")] + pub bin: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BaseManifest { + /// Link to the GitHub repository. + pub repository: String, + /// Alternative link for the project. Automatically detected if possible. + pub website: Option, + /// Markdown syntax for links to licenses. Automatically detected if possible. + pub license_markdown: Option, + /// Prefix of release tag. + pub tag_prefix: String, + /// Crate name, if this is Rust crate. + pub rust_crate: Option, + pub default_major_version: Option, + /// Asset name patterns. + pub asset_name: Option, + /// Path to binaries in archive. Default to `${tool}${exe}`. + pub bin: Option, + pub signing: Option, + #[serde(default)] + pub broken: Vec, + pub version_range: Option, + /// Use glibc build if host_env is gnu. + #[serde(default)] + pub prefer_linux_gnu: bool, + /// Check that the version is yanked not only when updating the manifest, + /// but also when running the action. + #[serde(default)] + pub immediate_yank_reflection: bool, + pub platform: BTreeMap, +} +impl BaseManifest { + /// Validate the manifest. + pub fn validate(&self) { + for bin in self.bin.iter().chain(self.platform.values().flat_map(|m| &m.bin)) { + assert!(!bin.as_slice().is_empty()); + for bin in bin.as_slice() { + let file_name = Path::new(bin).file_name().unwrap().to_str().unwrap(); + if !self.repository.ends_with("/xbuild") { + assert!( + !(file_name.contains("${version") || file_name.contains("${rust")), + "{bin}" + ); + } + } + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Signing { + pub kind: SigningKind, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +#[serde(deny_unknown_fields)] +pub enum SigningKind { + /// algorithm: minisign + /// public key: package.metadata.binstall.signing.pubkey at Cargo.toml + /// + MinisignBinstall, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BaseManifestPlatformInfo { + /// Asset name patterns. Default to the value at `BaseManifest::asset_name`. + pub asset_name: Option, + /// Path to binaries in archive. Default to the value at `BaseManifest::bin`. + pub bin: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StringOrArray { + String(String), + Array(Vec), +} + +impl StringOrArray { + #[must_use] + pub fn as_slice(&self) -> &[String] { + match self { + Self::String(s) => slice::from_ref(s), + Self::Array(v) => v, + } + } + #[must_use] + pub fn map(&self, mut f: impl FnMut(&String) -> String) -> Self { + match self { + Self::String(s) => Self::String(f(s)), + Self::Array(v) => Self::Array(v.iter().map(f).collect()), + } + } +} + +/// GitHub Actions Runner supports Linux (x86_64, aarch64, arm), Windows (x86_64, aarch64), +/// and macOS (x86_64, aarch64). +/// https://github.com/actions/runner/blob/v2.315.0/.github/workflows/build.yml#L21 +/// https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#supported-architectures-and-operating-systems-for-self-hosted-runners +/// +/// Note: +/// - Static-linked binaries compiled for linux-musl will also work on linux-gnu systems and are +/// usually preferred over linux-gnu binaries because they can avoid glibc version issues. +/// (rustc enables statically linking for linux-musl by default, except for mips.) +/// - Binaries compiled for x86_64 macOS will usually also work on aarch64 macOS. +/// - Binaries compiled for x86_64 Windows will usually also work on aarch64 Windows 11+. +/// - Ignore arm for now, as we need to consider the version and whether hard-float is supported. +/// https://github.com/rust-lang/rustup/pull/593 +/// https://github.com/cross-rs/cross/pull/1018 +/// Does it seem only armv7l+ is supported? +/// https://github.com/actions/runner/blob/v2.315.0/src/Misc/externals.sh#L189 +/// https://github.com/actions/runner/issues/688 +// TODO: support musl with dynamic linking like wasmtime 22.0.0+'s musl binaries: https://github.com/bytecodealliance/wasmtime/releases/tag/v22.0.0 +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum HostPlatform { + x86_64_linux_gnu, + x86_64_linux_musl, + x86_64_macos, + x86_64_windows, + aarch64_linux_gnu, + aarch64_linux_musl, + aarch64_macos, + aarch64_windows, +} + +impl HostPlatform { + #[must_use] + pub fn rust_target(self) -> &'static str { + match self { + Self::x86_64_linux_gnu => "x86_64-unknown-linux-gnu", + Self::x86_64_linux_musl => "x86_64-unknown-linux-musl", + Self::x86_64_macos => "x86_64-apple-darwin", + Self::x86_64_windows => "x86_64-pc-windows-msvc", + Self::aarch64_linux_gnu => "aarch64-unknown-linux-gnu", + Self::aarch64_linux_musl => "aarch64-unknown-linux-musl", + Self::aarch64_macos => "aarch64-apple-darwin", + Self::aarch64_windows => "aarch64-pc-windows-msvc", + } + } + #[must_use] + pub fn rust_target_arch(self) -> &'static str { + match self { + Self::aarch64_linux_gnu + | Self::aarch64_linux_musl + | Self::aarch64_macos + | Self::aarch64_windows => "aarch64", + Self::x86_64_linux_gnu + | Self::x86_64_linux_musl + | Self::x86_64_macos + | Self::x86_64_windows => "x86_64", + } + } + #[must_use] + pub fn rust_target_os(self) -> &'static str { + match self { + Self::aarch64_linux_gnu + | Self::aarch64_linux_musl + | Self::x86_64_linux_gnu + | Self::x86_64_linux_musl => "linux", + Self::aarch64_macos | Self::x86_64_macos => "macos", + Self::aarch64_windows | Self::x86_64_windows => "windows", + } + } + #[must_use] + pub fn exe_suffix(self) -> &'static str { + match self { + Self::x86_64_windows | Self::aarch64_windows => ".exe", + _ => "", + } + } +}