aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-08-18 22:39:38 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-08-18 22:39:38 +0300
commit9fac51722653fa0b0b6a5786166fb18c763424e0 (patch)
treea8efd3b30cf29a8fb11138b7444bb9baa0dac2fa /src
parent0bd445020ea19e0cea13f42a28212db7b2df196d (diff)
feat: make Windows generally available
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs30
-rw-r--r--src/config/model.rs34
-rw-r--r--src/main.rs4
-rw-r--r--src/update.rs73
4 files changed, 94 insertions, 47 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 7f813eb7..ac97edc5 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -216,12 +216,6 @@ fn channel_set_rejection(
channel: &str,
install_rejection: Option<&'static str>,
) -> Option<&'static str> {
- if cfg!(windows) && channel == "stable" {
- return Some(
- "stable channel is not available on Windows yet; Windows builds are preview-only",
- );
- }
-
if channel == "preview" {
return install_rejection;
}
@@ -1052,39 +1046,19 @@ mod tests {
}
#[test]
- fn channel_set_rejects_package_managed_preview_before_config_write() {
+ fn channel_set_only_applies_package_rejection_to_preview() {
assert_eq!(
super::channel_set_rejection("preview", Some("no preview")),
Some("no preview")
);
assert_eq!(
super::channel_set_rejection("stable", Some("no preview")),
- if cfg!(windows) {
- Some(
- "stable channel is not available on Windows yet; Windows builds are preview-only",
- )
- } else {
- None
- }
+ None
);
assert_eq!(super::channel_set_rejection("preview", None), None);
}
#[test]
- fn channel_set_rejects_stable_only_on_windows() {
- assert_eq!(
- super::channel_set_rejection("stable", None),
- if cfg!(windows) {
- Some(
- "stable channel is not available on Windows yet; Windows builds are preview-only",
- )
- } else {
- None
- }
- );
- }
-
- #[test]
fn channel_set_skips_self_update_for_package_manager_guidance() {
assert_eq!(
super::channel_set_install_action(Some("use package manager")),
diff --git a/src/config/model.rs b/src/config/model.rs
index ad779d26..c10cd733 100644
--- a/src/config/model.rs
+++ b/src/config/model.rs
@@ -47,7 +47,11 @@ impl Default for UpdateConfig {
}
fn default_update_channel() -> UpdateChannelConfig {
- if cfg!(windows) {
+ default_update_channel_for_build(cfg!(windows), crate::build_info::is_preview())
+}
+
+fn default_update_channel_for_build(is_windows: bool, is_preview: bool) -> UpdateChannelConfig {
+ if is_windows && is_preview {
UpdateChannelConfig::Preview
} else {
UpdateChannelConfig::Stable
@@ -1251,21 +1255,33 @@ manifest_check = false
assert!(!config.update.manifest_check);
}
- #[cfg(windows)]
#[test]
- fn windows_update_config_defaults_to_preview() {
+ fn update_channel_default_follows_windows_build_identity() {
+ assert_eq!(
+ default_update_channel_for_build(true, true),
+ UpdateChannelConfig::Preview
+ );
+ assert_eq!(
+ default_update_channel_for_build(true, false),
+ UpdateChannelConfig::Stable
+ );
+ assert_eq!(
+ default_update_channel_for_build(false, true),
+ UpdateChannelConfig::Stable
+ );
+ }
+
+ #[test]
+ fn missing_update_channel_uses_build_default() {
let empty: Config = toml::from_str("").unwrap();
let without_update_channel: Config =
toml::from_str("[update]\nversion_check = false").unwrap();
- assert_eq!(
- Config::default().update.channel,
- UpdateChannelConfig::Preview
- );
- assert_eq!(empty.update.channel, UpdateChannelConfig::Preview);
+ assert_eq!(Config::default().update.channel, default_update_channel());
+ assert_eq!(empty.update.channel, default_update_channel());
assert_eq!(
without_update_channel.update.channel,
- UpdateChannelConfig::Preview
+ default_update_channel()
);
}
diff --git a/src/main.rs b/src/main.rs
index d2fc0595..36b1d021 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -153,8 +153,8 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
[update]
# Update channel used by background version checks and `herdr update`.
-# Defaults to "stable" on Linux/macOS and "preview" on Windows.
-# Set explicitly to choose stable releases or opt-in preview builds.
+# Stable builds default to "stable". Windows preview builds default to "preview"
+# so existing preview installs stay there until explicitly switched.
# channel = "stable"
# Check herdr.dev for new Herdr versions in the background.
diff --git a/src/update.rs b/src/update.rs
index 161386f9..5359944a 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -511,6 +511,14 @@ fn release_info_from_preview_manifest(
}
/// Check the hosted update manifest for the latest release. Returns release info if newer.
+fn first_windows_stable_is_pending(
+ manifest: &UpdateManifest,
+ is_windows: bool,
+ installed_is_preview: bool,
+) -> bool {
+ is_windows && installed_is_preview && !manifest.assets.contains_key("windows-x86_64")
+}
+
fn check_latest() -> Result<Option<ReleaseInfo>, String> {
let channel = UpdateChannel::configured();
if channel == UpdateChannel::Preview {
@@ -518,6 +526,10 @@ fn check_latest() -> Result<Option<ReleaseInfo>, String> {
}
let manifest = fetch_update_manifest()?;
+ if first_windows_stable_is_pending(&manifest, cfg!(windows), crate::build_info::is_preview()) {
+ tracing::info!("waiting for the first stable Windows release");
+ return Ok(None);
+ }
let release = release_info_from_manifest(&manifest)?;
if let Some(release) = &release {
if let Some(metadata) = manifest.metadata_for_version(&release.version) {
@@ -2058,12 +2070,6 @@ fn homebrew_cellar_keg_root(path: &Path) -> Option<PathBuf> {
/// Manual self-update command (`herdr update`).
pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
let channel = UpdateChannel::configured();
- #[cfg(windows)]
- if channel == UpdateChannel::Stable {
- return Err(
- "Windows builds are preview-only for now; run `herdr channel set preview`".into(),
- );
- }
if is_homebrew_managed_install() {
if channel == UpdateChannel::Preview {
@@ -3553,6 +3559,35 @@ mod tests {
}
#[test]
+ fn preview_windows_build_waits_for_first_stable_asset() {
+ let without_windows: UpdateManifest = serde_json::from_str(
+ r#"{"version":"9.9.9","notes":"notes","assets":{},"announcement":null}"#,
+ )
+ .unwrap();
+ assert!(first_windows_stable_is_pending(
+ &without_windows,
+ true,
+ true
+ ));
+ assert!(!first_windows_stable_is_pending(
+ &without_windows,
+ true,
+ false
+ ));
+ assert!(!first_windows_stable_is_pending(
+ &without_windows,
+ false,
+ true
+ ));
+
+ let with_windows: UpdateManifest = serde_json::from_str(
+ r#"{"version":"9.9.9","notes":"notes","assets":{"windows-x86_64":"https://example.com/herdr-windows-x86_64.zip"},"announcement":null}"#,
+ )
+ .unwrap();
+ assert!(!first_windows_stable_is_pending(&with_windows, true, true));
+ }
+
+ #[test]
fn checked_in_website_manifest_matches_update_schema() {
#[derive(Deserialize)]
struct LegacyUpdateManifest {
@@ -3562,7 +3597,7 @@ mod tests {
let json = include_str!("../website/latest.json");
let legacy: LegacyUpdateManifest = serde_json::from_str(json)
.expect("website/latest.json should keep legacy string asset URLs");
- assert_eq!(legacy.assets.len(), 4);
+ assert!(legacy.assets.len() >= 4);
let manifest: UpdateManifest =
serde_json::from_str(json).expect("website/latest.json should match updater schema");
@@ -3576,7 +3611,7 @@ mod tests {
// current unreleased checkout. Its protocol is updated by the release
// flow together with the release assets.
assert!(manifest.protocol.is_some());
- assert_eq!(manifest.assets.len(), 4);
+ assert!(manifest.assets.len() >= 4);
assert!(manifest.releases.contains_key(&manifest.version));
for target in [
@@ -3605,6 +3640,15 @@ mod tests {
);
}
+ if let Some(windows) = manifest.assets.get("windows-x86_64") {
+ assert!(windows.url.ends_with("/herdr-windows-x86_64.zip"));
+ assert_eq!(
+ manifest.sha256.get("windows-x86_64").map(String::len),
+ Some(64),
+ "missing SHA-256 checksum for windows-x86_64"
+ );
+ }
+
for (version, release) in &manifest.releases {
let assets = release
.get("assets")
@@ -3632,6 +3676,19 @@ mod tests {
"unexpected asset name for {version} {target}: {url}"
);
}
+ if let Some(windows) = assets.get("windows-x86_64") {
+ let windows: AssetRef = serde_json::from_value(windows.clone())
+ .unwrap_or_else(|_| panic!("invalid Windows asset for release {version}"));
+ assert!(windows.url.ends_with("/herdr-windows-x86_64.zip"));
+ let checksums = release
+ .get("sha256")
+ .and_then(serde_json::Value::as_object)
+ .unwrap_or_else(|| panic!("missing checksums for release {version}"));
+ assert!(checksums
+ .get("windows-x86_64")
+ .and_then(serde_json::Value::as_str)
+ .is_some_and(|value| value.len() == 64));
+ }
}
}
}