Switch to unambiguous version detection, refine release notes link derivation

This commit is contained in:
Simon Repp
2026-08-11 09:57:03 +02:00
parent ed52375b6c
commit d58f7e3342
4 changed files with 186 additions and 87 deletions
+4 -4
View File
@@ -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(
<header data-tauri-drag-region>
{SPLASH_SVG}
<button class="close">{close_icon}</button>
<a href="{CHANGES_URL}" target="_blank">
<a href="{RELEASE_NOTES}" target="_blank">
{VERSION_ADAPTIVE_PATCH}
</a>
</header>
@@ -196,7 +196,7 @@ pub fn render(
<a href="https://hyper8.org/#donate" target="_blank">
{t_donate}
</a>
<a href="{CHANGES_URL}" target="_blank">
<a href="{RELEASE_NOTES}" target="_blank">
{t_whats_new}
</a>
</div>
+168 -63
View File
@@ -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::<u16>().ok()?;
let (minor, remainder) = remainder.split_once('.')?;
let _ = minor.parse::<u16>().ok()?;
match remainder.split_once(|c| c == '~' || c == '-') {
Some((patch, label)) => {
let _ = patch.parse::<u16>().ok()?;
Some((major, minor, patch, Some(label)))
}
None => {
let _ = remainder.parse::<u16>().ok()?;
Some((major, minor, remainder, None))
}
}
}
fn git_describe() -> Option<String> {
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);
}
}
+11 -17
View File
@@ -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");
+3 -3
View File
@@ -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 {
<a class="donate" href="https://hyper8.org/#donate" target="_blank">
{t_donate}
</a>
<a class="version" href="{CHANGES_URL}" target="_blank">
<a class="version" href="{RELEASE_NOTES}" target="_blank">
<span>{VERSION_ADAPTIVE_PATCH}</span>
</a>
</div>