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(
@@ -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 {
{t_donate}
-
+
{VERSION_ADAPTIVE_PATCH}