From 1ef1e14c215683f91e4c3d07aa2318e6085719c2 Mon Sep 17 00:00:00 2001 From: Jiahao XU <30436523+NobodyXu@users.noreply.github.com> Date: Tue, 28 Jan 2025 18:02:39 +1100 Subject: [PATCH] Extract install-action-manifest-schema and publish to crates-io (#657) Signed-off-by: Jiahao XU Co-authored-by: Taiki Endo --- .deny.toml | 2 + .github/.cspell/project-dictionary.txt | 1 + .github/workflows/manifest_sync.yml | 51 +++ Cargo.toml | 2 +- tools/ci/checkout-manifest-schema-branch.sh | 28 ++ tools/codegen/Cargo.toml | 1 + tools/codegen/src/lib.rs | 379 +------------------ tools/manifest-schema/Cargo.toml | 17 + tools/manifest-schema/LICENSE-APACHE | 177 +++++++++ tools/manifest-schema/LICENSE-MIT | 23 ++ tools/manifest-schema/src/lib.rs | 391 ++++++++++++++++++++ 11 files changed, 694 insertions(+), 378 deletions(-) create mode 100644 .deny.toml create mode 100644 .github/workflows/manifest_sync.yml create mode 100755 tools/ci/checkout-manifest-schema-branch.sh create mode 100644 tools/manifest-schema/Cargo.toml create mode 100644 tools/manifest-schema/LICENSE-APACHE create mode 100644 tools/manifest-schema/LICENSE-MIT create mode 100644 tools/manifest-schema/src/lib.rs diff --git a/.deny.toml b/.deny.toml new file mode 100644 index 00000000..ca2d55e7 --- /dev/null +++ b/.deny.toml @@ -0,0 +1,2 @@ +[licenses] +allow = ["MIT", "Apache-2.0", "Unicode-3.0"] diff --git a/.github/.cspell/project-dictionary.txt b/.github/.cspell/project-dictionary.txt index 0864eb02..e29477ee 100644 --- a/.github/.cspell/project-dictionary.txt +++ b/.github/.cspell/project-dictionary.txt @@ -30,5 +30,6 @@ syft udeps wasmtime watchexec +worktree xbuild xscale diff --git a/.github/workflows/manifest_sync.yml b/.github/workflows/manifest_sync.yml new file mode 100644 index 00000000..7ebc2822 --- /dev/null +++ b/.github/workflows/manifest_sync.yml @@ -0,0 +1,51 @@ +name: Manifest Synchronization + +permissions: + contents: read + +on: + release: + types: [released] + +env: + WORKSPACE: /tmp/workspace + +defaults: + run: + shell: bash --noprofile --norc -CeEuxo pipefail {0} + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true + + - name: Checkout manifest-schema branch + run: ./tools/ci/checkout-manifest-schema-branch.sh "${WORKSPACE}" + + - name: Copy over schema + run: cp -- ./manifests/* "${WORKSPACE}" + + - name: Stage changes + working-directory: ${{ env.WORKSPACE }} + run: git add . + + - name: Show diff + working-directory: ${{ env.WORKSPACE }} + run: git diff HEAD + + - name: Detect changes + id: changes + working-directory: ${{ env.WORKSPACE }} + run: | + # This output boolean tells us if the dependencies have actually changed + printf "count=%s\n" "$(git status --porcelain=v1 | wc -l)" >>"${GITHUB_OUTPUT}" + + - name: Commit and push + # Only push if changes exist + if: steps.changes.outputs.count > 0 + working-directory: ${{ env.WORKSPACE }} + run: | + git commit -m "Update manifest schema" && git push origin HEAD diff --git a/Cargo.toml b/Cargo.toml index 6625fcc9..5c5ebff6 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/ci/checkout-manifest-schema-branch.sh b/tools/ci/checkout-manifest-schema-branch.sh new file mode 100755 index 00000000..f96630ab --- /dev/null +++ b/tools/ci/checkout-manifest-schema-branch.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 OR MIT +set -CeEuo pipefail +IFS=$'\n\t' +trap -- 's=$?; printf >&2 "%s\n" "${0##*/}:${LINENO}: \`${BASH_COMMAND}\` exit with ${s}"; exit ${s}' ERR +cd -- "$(dirname -- "$0")" + +version="$(cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | select(.name == "install-action-manifest-schema") | .version')" +if [[ "${version}" == "0."* ]]; then + schema_version="0.$(cut -d. -f2 <<<"${version}")" +else + schema_version="$(cut -d. -f1 <<<"${version}")" +fi +branch="manifest-schema-${schema_version}" + +git worktree add --force "${1?}" +cd -- "$1" + +if git fetch origin "${branch}"; then + git checkout "origin/${branch}" -B "${branch}" +elif ! git checkout "${branch}"; then + # New branch with no history. Credit: https://stackoverflow.com/a/13969482 + git checkout --orphan "${branch}" + git rm -rf . || true + git config --local user.name github-actions + git config --local user.email github-actions@github.com + git commit -m 'Initial commit' --allow-empty +fi diff --git a/tools/codegen/Cargo.toml b/tools/codegen/Cargo.toml index 36c9789f..f5e57e00 100644 --- a/tools/codegen/Cargo.toml +++ b/tools/codegen/Cargo.toml @@ -4,6 +4,7 @@ edition = "2021" default-run = "generate-manifest" [dependencies] +install-action-manifest-schema = { path = "../manifest-schema" } anyhow = "1" flate2 = "1" fs-err = "3" diff --git a/tools/codegen/src/lib.rs b/tools/codegen/src/lib.rs index 173c7d60..a47924d4 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 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.321.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.321.0/src/Misc/externals.sh#L178 -/// 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 00000000..e82d90dd --- /dev/null +++ b/tools/manifest-schema/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "install-action-manifest-schema" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +anyhow = "1" +semver = { version = "1", features = ["serde"] } +serde = "1" +serde_derive = "1" + +[lints] +workspace = true + +[package.metadata.cargo_check_external_types] +allowed_external_types = ["semver::*", "serde::*"] diff --git a/tools/manifest-schema/LICENSE-APACHE b/tools/manifest-schema/LICENSE-APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/tools/manifest-schema/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/tools/manifest-schema/LICENSE-MIT b/tools/manifest-schema/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/tools/manifest-schema/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/tools/manifest-schema/src/lib.rs b/tools/manifest-schema/src/lib.rs new file mode 100644 index 00000000..5bc1e951 --- /dev/null +++ b/tools/manifest-schema/src/lib.rs @@ -0,0 +1,391 @@ +// 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}; + +#[must_use] +pub fn get_manifest_schema_branch_name() -> &'static str { + if env!("CARGO_PKG_VERSION_MAJOR") == "0" { + concat!("manifest-schema-0.", env!("CARGO_PKG_VERSION_MINOR")) + } else { + concat!("manifest-schema-", env!("CARGO_PKG_VERSION_MAJOR")) + } +} + +#[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). +/// +/// +/// +/// 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. +/// +/// +/// Does it seem only armv7l+ is supported? +/// +/// +// TODO: support musl with dynamic linking like wasmtime 22.0.0+'s musl binaries: +#[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", + _ => "", + } + } +}