diff --git a/desktop/src/commands/render.rs b/desktop/src/commands/render.rs index a23d970..cf66ad5 100644 --- a/desktop/src/commands/render.rs +++ b/desktop/src/commands/render.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Simon Repp +// SPDX-FileCopyrightText: 2025-2026 Simon Repp // SPDX-License-Identifier: AGPL-3.0-or-later use indoc::formatdoc; @@ -9,7 +9,7 @@ use tauri::State; use hyper8_core::Theme; use hyper8_core::{AVAILABLE_LANGUAGES, SPLASH_SVG}; use hyper8_core::util::html_escape_outside_attribute; -use hyper8_version::{CHANGES_URL, VERSION_ADAPTIVE_PATCH}; +use hyper8_version::{RELEASE_NOTES, VERSION_ADAPTIVE_PATCH}; use crate::{EditorMode, LauncherState}; @@ -181,7 +181,7 @@ pub fn render(
{SPLASH_SVG} - + {VERSION_ADAPTIVE_PATCH}
@@ -196,7 +196,7 @@ pub fn render( {t_donate} - + {t_whats_new} diff --git a/version/build.rs b/version/build.rs index d16b04e..a1cf854 100644 --- a/version/build.rs +++ b/version/build.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024-2025 Simon Repp +// SPDX-FileCopyrightText: 2024-2026 Simon Repp // SPDX-License-Identifier: AGPL-3.0-or-later use std::env; @@ -6,79 +6,184 @@ use std::process::Command; const CHANGES_INDEX_URL: &str = "https://hyper8.org/changes/"; +/// For non-semantic versions we link to the release notes index page +fn export_generic_release_notes() { + let url = CHANGES_INDEX_URL; + export_release_notes_variable(url); +} + +/// For regular semantic versions our convention is to link to release +/// notes in the "[major].[minor].[patch]/" directory under the release +/// notes index page +fn export_semantic_release_notes( + major: &str, + minor: &str, + patch: &str +) { + let url = format!("{CHANGES_INDEX_URL}{major}.{minor}.{patch}/"); + export_release_notes_variable(&url); +} + +fn export_semantic_version( + major: &str, + minor: &str, + patch: &str +) { + let revision = git_revision(); + + let version_adaptive_patch = if patch == "0" { + format!("{major}.{minor}") + } else { + format!("{major}.{minor}.{patch}") + }; + + let version_with_patch = format!("{major}.{minor}.{patch}"); + let version_with_patch_and_revision = format!("{major}.{minor}.{patch} ({revision})"); + let version_without_patch = format!("{major}.{minor}"); + + export_version_variables( + &version_adaptive_patch, + &version_with_patch, + &version_with_patch_and_revision, + &version_without_patch + ); +} + +fn export_special_version(version: &str) { + let revision = git_revision(); + + let version_with_revision = if version == revision { + version.to_string() + } else { + format!("{version} ({revision})") + }; + + export_version_variables( + version, + version, + &version_with_revision, + version + ); +} + +fn export_release_notes_variable(url: &str) { + println!("cargo:rustc-env=HYPER8_RELEASE_NOTES={url}"); +} + +fn export_version_variables( + version_adaptive_patch: &str, + version_with_patch: &str, + version_with_patch_and_revision: &str, + version_without_patch: &str +) { + println!("cargo:rustc-env=HYPER8_VERSION_ADAPTIVE_PATCH={version_adaptive_patch}"); + println!("cargo:rustc-env=HYPER8_VERSION_WITH_PATCH={version_with_patch}"); + println!("cargo:rustc-env=HYPER8_VERSION_WITH_PATCH_AND_REVISION={version_with_patch_and_revision}"); + println!("cargo:rustc-env=HYPER8_VERSION_WITHOUT_PATCH={version_without_patch}"); +} + +// TODO: According to the canonical https://semver.org/ specification, +// optional additional labels containing build/pre-release metadata may only +// be separated by a narrower subset of characters than we allow (and use). +// We might want to adopt these rules eventually. + +/// Extracts and returns the major, minor and patch number and optional +/// pre-release/build label from the passed version string if it follows +/// a semantic format recognized by us, such as: +/// - 1.2.3 +/// - 1.2.3~beta1 +/// - 1.2.3-preview1 +fn extract_semantic_version(version: &str) -> Option<(&str, &str, &str, Option<&str>)> { + let (major, remainder) = version.split_once('.')?; + let _ = major.parse::().ok()?; + + let (minor, remainder) = remainder.split_once('.')?; + let _ = minor.parse::().ok()?; + + match remainder.split_once(|c| c == '~' || c == '-') { + Some((patch, label)) => { + let _ = patch.parse::().ok()?; + Some((major, minor, patch, Some(label))) + } + None => { + let _ = remainder.parse::().ok()?; + Some((major, minor, remainder, None)) + } + } +} + +fn git_describe() -> Option { + let mut git = Command::new("git"); + + git.args(["describe"]); + + if let Ok(output) = git.output() && output.status.success() { + Some(String::from_utf8(output.stdout).unwrap()) + } else { + None + } +} + /// Call git to determine the 7-digit short hash of the current revision. /// If git is not available, we fall back to "unknown revision". /// The output is stored in HYPER8_REVISION. -fn compute_revision() { +fn git_revision() -> String { let mut git = Command::new("git"); git.args(["rev-parse", "--short", "HEAD"]); - let revision = match git.output() { - Ok(output) if output.status.success() => String::from_utf8(output.stdout).unwrap(), - _ => String::from("unknown revision") - }; - - println!("cargo:rustc-env=HYPER8_REVISION={revision}"); + if let Ok(output) = git.output() && output.status.success() { + String::from_utf8(output.stdout).unwrap() + } else { + String::from("unknown revision") + } } -fn main() { - compute_revision(); - compute_version(); -} - -/// Provides three variables to the consecutive build step: -/// - HYPER8_VERSION_ADAPTIVE_PATCH (With patch only if it is a patch release, e.g. "1.2.1") +/// Provides variables to the consecutive build step: +/// - HYPER8_RELEASE_NOTES (e.g. https://hyper8.org/changes/1.2.3/) +/// - HYPER8_VERSION_ADAPTIVE_PATCH (With patch only if it is a patch release, e.g. "1.2.3") /// - HYPER8_VERSION_WITH_PATCH (Always with patch, e.g. "1.2.0") +/// - HYPER8_VERSION_WITH_PATCH_AND_REVISION (Always with patch and revision, e.g. "1.2.3 (0abcdef)") /// - HYPER8_VERSION_WITHOUT_PATCH (Always without patch, e.g. "1.2") /// -/// If HYPER8_VERSION is used to override the version, all three variables -/// will contain the full version, e.g. "2.0.0~pre1". -fn compute_version() { - let changes_url; - let version_adaptive_patch; - let version_with_patch; - let version_without_patch; - - if let Ok(override_version) = env::var("HYPER8_VERSION") { - // An override version can be somewhat arbitrary, or refer to a - // version for which there are no release notes available online yet, - // so here we always fall back to the more general release notes - // index page to avoid linking to a potentially unavailable page. - changes_url = CHANGES_INDEX_URL.to_string(); - - version_adaptive_patch = override_version.clone(); - version_with_patch = override_version.clone(); - version_without_patch = override_version; - } else { - // For regular tag-based releases our convention currently is that - // there is always a dedicated release notes page available for the - // given tag at the major.minor.patch/ directory under the release - // notes index page. - changes_url = format!("{CHANGES_INDEX_URL}{}/", env!("CARGO_PKG_VERSION")); - - version_adaptive_patch = if env!("CARGO_PKG_VERSION_PATCH") == "0" { - concat!( - env!("CARGO_PKG_VERSION_MAJOR"), - '.', - env!("CARGO_PKG_VERSION_MINOR") - ).to_string() - } else { - env!("CARGO_PKG_VERSION").to_string() - }; - - version_with_patch = env!("CARGO_PKG_VERSION").to_string(); - - version_without_patch = concat!( - env!("CARGO_PKG_VERSION_MAJOR"), - '.', - env!("CARGO_PKG_VERSION_MINOR") - ).to_string(); - } - +/// If no semantic version is available we use various fallbacks to provide +/// some kind of version information (e.g. only the git revision). +fn main() { println!("cargo:rerun-if-env-changed=HYPER8_VERSION"); - println!("cargo:rustc-env=HYPER8_CHANGES_URL={changes_url}"); - println!("cargo:rustc-env=HYPER8_VERSION_ADAPTIVE_PATCH={version_adaptive_patch}"); - println!("cargo:rustc-env=HYPER8_VERSION_WITH_PATCH={version_with_patch}"); - println!("cargo:rustc-env=HYPER8_VERSION_WITHOUT_PATCH={version_without_patch}"); + + if let Ok(version) = env::var("HYPER8_VERSION") { + // Version was explicitly defined by environment variable + + if let Some((major, minor, patch, label)) = extract_semantic_version(&version) { + export_semantic_release_notes(major, minor, patch); + + if label.is_none() { + export_semantic_version(major, minor, patch); + } else { + export_special_version(&version); + } + } else { + export_generic_release_notes(); + export_special_version(&version); + } + } else if git_describe().is_some_and(|name| name == env!("CARGO_PKG_VERSION")) { + // Version is clearly implied by both the version declared in the + // cargo manifest and the matching version in the tag of current + // commit + + let major = env!("CARGO_PKG_VERSION_MAJOR"); + let minor = env!("CARGO_PKG_VERSION_MINOR"); + let patch = env!("CARGO_PKG_VERSION_PATCH"); + + export_semantic_release_notes(major, minor, patch); + export_semantic_version(major, minor, patch); + } else { + // We can not tell a version with any certainty, so we use the git + // revision as a unique version identifier instead + + let revision = git_revision(); + + export_generic_release_notes(); + export_special_version(&revision); + } } diff --git a/version/src/lib.rs b/version/src/lib.rs index e5c727c..59c67c0 100644 --- a/version/src/lib.rs +++ b/version/src/lib.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Simon Repp +// SPDX-FileCopyrightText: 2025-2026 Simon Repp // SPDX-License-Identifier: AGPL-3.0-or-later //! Build any hyper8 target (cli, docs, etc.) with the environment variable @@ -8,28 +8,22 @@ //! For instance to build the docs for version "2.0.0~pre1": //! "HYPER8_VERSION=2.0.0~pre1 cargo run -p hyper8-docs -- [output_path]" -/// The link https://hyper8.org/changes/[major.minor.patch]/ for -/// regular tag-based releases. If the version is being overridden using -/// HYPER8_VERSION, it falls back to the more generic release notes index -/// page (https://hyper8.org/changes/). -pub const CHANGES_URL: &str = env!("HYPER8_CHANGES_URL"); - -/// 7-digit short commit hash or "unknown revision" -pub const REVISION: &str = env!("HYPER8_REVISION"); +/// The link https://hyper8.org/changes/[major.minor.patch]/ if a semantic +/// version is present either in the tag of the current commit (with a +/// matching version present in the cargo manifest) or passed through the +/// HYPER8_VERSION environment variable. If no semantic version is detected +/// it falls back to the more generic release notes index page at +/// https://hyper8.org/changes/. +pub const RELEASE_NOTES: &str = env!("HYPER8_RELEASE_NOTES"); /// Adaptive version, e.g. "0.23" or "0.23.1" (leaves out patch for major/minor releases) pub const VERSION_ADAPTIVE_PATCH: &str = env!("HYPER8_VERSION_ADAPTIVE_PATCH"); -/// E.g. Full version, "0.23.0" or "1.6.3" +/// Full version, e.g. "0.23.0" or "1.6.3" pub const VERSION_WITH_PATCH: &str = env!("HYPER8_VERSION_WITH_PATCH"); -/// E.g. "1.2.3 (0abcdef)" -pub const VERSION_WITH_PATCH_AND_REVISION: &str = concat!( - env!("HYPER8_VERSION_WITH_PATCH"), - " (", - env!("HYPER8_REVISION"), - ")" -); +/// Full version with 7-digit short commit hash, e.g. "1.2.3 (0abcdef)" +pub const VERSION_WITH_PATCH_AND_REVISION: &str = env!("HYPER8_VERSION_WITH_PATCH_AND_REVISION"); /// Only the major and minor version, e.g. "0.23" or "1.6" pub const VERSION_WITHOUT_PATCH: &str = env!("HYPER8_VERSION_WITHOUT_PATCH"); diff --git a/web/src/widgets/splash_screen.rs b/web/src/widgets/splash_screen.rs index c68d701..0b94247 100644 --- a/web/src/widgets/splash_screen.rs +++ b/web/src/widgets/splash_screen.rs @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2024-2025 Simon Repp +// SPDX-FileCopyrightText: 2024-2026 Simon Repp // SPDX-License-Identifier: AGPL-3.0-or-later use indoc::formatdoc; use hyper8_core::SPLASH_SVG; -use hyper8_version::{CHANGES_URL, VERSION_ADAPTIVE_PATCH}; +use hyper8_version::{RELEASE_NOTES, VERSION_ADAPTIVE_PATCH}; use crate::SessionState; @@ -31,7 +31,7 @@ pub fn splash_screen(session_state: &SessionState) -> String { - + {VERSION_ADAPTIVE_PATCH}