11// Copyright © 2025-2026 OpenVCS Contributors
22// SPDX-License-Identifier: GPL-3.0-or-later
3+ use serde:: Deserialize ;
34use std:: { env, fs, path:: PathBuf , process:: Command } ;
45
6+ fn load_channel_metadata ( ) -> ChannelMetadata {
7+ let manifest_dir = PathBuf :: from ( env:: var ( "CARGO_MANIFEST_DIR" ) . expect ( "CARGO_MANIFEST_DIR" ) ) ;
8+ let config_path = manifest_dir. join ( "../channel-metadata.json" ) ;
9+ let data = fs:: read_to_string ( & config_path) . expect ( "read channel-metadata.json" ) ;
10+ serde_json:: from_str ( & data) . expect ( "parse channel-metadata.json" )
11+ }
12+
13+ #[ derive( Clone , Debug , Deserialize ) ]
14+ struct ChannelMetadata {
15+ channels : Channels ,
16+ }
17+
18+ #[ derive( Clone , Debug , Deserialize ) ]
19+ struct Channels {
20+ stable : ChannelEntry ,
21+ beta : ChannelEntry ,
22+ nightly : ChannelEntry ,
23+ }
24+
25+ #[ derive( Clone , Debug , Deserialize ) ]
26+ struct ChannelEntry {
27+ slug : String ,
28+ #[ serde( rename = "mainBinaryName" ) ]
29+ main_binary_name : String ,
30+ #[ serde( rename = "productName" ) ]
31+ product_name : String ,
32+ identifier : String ,
33+ #[ serde( rename = "windowTitle" ) ]
34+ window_title : String ,
35+ #[ serde( rename = "updaterEndpoints" ) ]
36+ updater_endpoints : Vec < String > ,
37+ }
38+
39+ /// Channel-specific metadata used for generated desktop bundles.
40+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
41+ struct ChannelConfig {
42+ /// Normalized channel slug used across build and runtime.
43+ slug : & ' static str ,
44+ /// Binary and bundle stem without spaces.
45+ main_binary_name : & ' static str ,
46+ /// Human-facing desktop product name.
47+ product_name : & ' static str ,
48+ /// Tauri bundle identifier.
49+ identifier : & ' static str ,
50+ /// Main window title.
51+ window_title : & ' static str ,
52+ /// Updater endpoint URLs.
53+ updater_endpoints : & ' static [ & ' static str ] ,
54+ }
55+
56+ impl ChannelConfig {
57+ /// Resolves normalized channel metadata from an arbitrary environment value.
58+ ///
59+ /// # Parameters
60+ /// - `raw`: Raw environment value.
61+ ///
62+ /// # Returns
63+ /// - Stable metadata when the input is missing or unknown.
64+ /// - Beta or nightly metadata for recognized channel names.
65+ fn from_env_value ( raw : & str , metadata : & ChannelMetadata ) -> Self {
66+ let slug = raw. trim ( ) . to_ascii_lowercase ( ) ;
67+ let entry = match slug. as_str ( ) {
68+ "beta" => & metadata. channels . beta ,
69+ "nightly" => & metadata. channels . nightly ,
70+ _ => {
71+ if !raw. trim ( ) . is_empty ( ) {
72+ eprintln ! ( "Warning: Unknown channel '{raw}', defaulting to stable" ) ;
73+ }
74+ & metadata. channels . stable
75+ }
76+ } ;
77+ Self :: from_entry ( entry)
78+ }
79+
80+ fn from_entry ( entry : & ChannelEntry ) -> Self {
81+ let endpoints: Vec < & ' static str > = entry
82+ . updater_endpoints
83+ . iter ( )
84+ . map ( |s| Box :: leak ( s. to_string ( ) . into_boxed_str ( ) ) as & str )
85+ . collect ( ) ;
86+ Self {
87+ // Box::leak is acceptable here - this is a build script that runs once.
88+ // Memory is leaked for the program duration but that's fine for a build script.
89+ slug : Box :: leak ( entry. slug . clone ( ) . into_boxed_str ( ) ) ,
90+ main_binary_name : Box :: leak ( entry. main_binary_name . clone ( ) . into_boxed_str ( ) ) ,
91+ product_name : Box :: leak ( entry. product_name . clone ( ) . into_boxed_str ( ) ) ,
92+ identifier : Box :: leak ( entry. identifier . clone ( ) . into_boxed_str ( ) ) ,
93+ window_title : Box :: leak ( entry. window_title . clone ( ) . into_boxed_str ( ) ) ,
94+ updater_endpoints : Box :: leak ( endpoints. into_boxed_slice ( ) ) ,
95+ }
96+ }
97+ }
98+
599/// Returns whether the build is running for Flatpak packaging.
6100fn is_flatpak_build ( ) -> bool {
7101 matches ! (
@@ -131,37 +225,36 @@ fn main() {
131225 let data = fs:: read_to_string ( & base) . expect ( "read tauri.conf.json" ) ;
132226 let mut json: serde_json:: Value = serde_json:: from_str ( & data) . expect ( "parse tauri.conf.json" ) ;
133227
134- // Compute channel based on environment; default to stable
135- let chan = env:: var ( "OPENVCS_UPDATE_CHANNEL" ) . unwrap_or_else ( |_| "stable" . into ( ) ) ;
136-
137- // Repository URL (can be overridden via env var for forks)
138- let repo =
139- env:: var ( "OPENVCS_REPO" ) . unwrap_or_else ( |_| "https://github.com/Jordonbc/OpenVCS" . into ( ) ) ;
140-
141- // Build update URLs from repository
142- let stable =
143- serde_json:: Value :: String ( format ! ( "{}/releases/latest/download/latest.json" , repo) ) ;
144- let beta = serde_json:: Value :: String ( format ! (
145- "{}/releases/download/openvcs-beta/latest.json" ,
146- repo
147- ) ) ;
148- let nightly = serde_json:: Value :: String ( format ! (
149- "{}/releases/download/openvcs-nightly/latest.json" ,
150- repo
151- ) ) ;
228+ // Compute channel based on environment; default to stable.
229+ let channel_metadata = load_channel_metadata ( ) ;
230+ let channel = ChannelConfig :: from_env_value (
231+ & env:: var ( "OPENVCS_UPDATE_CHANNEL" ) . unwrap_or_else ( |_| "stable" . into ( ) ) ,
232+ & channel_metadata,
233+ ) ;
152234
153235 // Navigate: plugins.updater.endpoints
154236 if let Some ( plugins) = json. get_mut ( "plugins" ) {
155237 if let Some ( updater) = plugins. get_mut ( "updater" ) {
156- let endpoints = match chan. as_str ( ) {
157- // Beta: check beta first, then stable
158- "beta" => serde_json:: Value :: Array ( vec ! [ beta. clone( ) , stable. clone( ) ] ) ,
159- // Nightly: check nightly first, then stable
160- "nightly" => serde_json:: Value :: Array ( vec ! [ nightly. clone( ) , stable. clone( ) ] ) ,
161- // Stable: stable only
162- _ => serde_json:: Value :: Array ( vec ! [ stable. clone( ) ] ) ,
163- } ;
164- updater[ "endpoints" ] = endpoints;
238+ let endpoints: Vec < serde_json:: Value > = channel
239+ . updater_endpoints
240+ . iter ( )
241+ . map ( |s| serde_json:: Value :: String ( ( * s) . to_string ( ) ) )
242+ . collect ( ) ;
243+ updater[ "endpoints" ] = serde_json:: Value :: Array ( endpoints) ;
244+ }
245+ }
246+
247+ json[ "mainBinaryName" ] = serde_json:: Value :: String ( channel. main_binary_name . into ( ) ) ;
248+ json[ "productName" ] = serde_json:: Value :: String ( channel. product_name . into ( ) ) ;
249+ json[ "identifier" ] = serde_json:: Value :: String ( channel. identifier . into ( ) ) ;
250+ if let Some ( app) = json. get_mut ( "app" ) {
251+ if let Some ( windows) = app
252+ . get_mut ( "windows" )
253+ . and_then ( |value| value. as_array_mut ( ) )
254+ {
255+ if let Some ( main_window) = windows. first_mut ( ) {
256+ main_window[ "title" ] = serde_json:: Value :: String ( channel. window_title . into ( ) ) ;
257+ }
165258 }
166259 }
167260
@@ -196,6 +289,7 @@ fn main() {
196289 // Provide the generated config via inline JSON env var (must be single-line)
197290 let inline = serde_json:: to_string ( & json) . unwrap ( ) ;
198291 println ! ( "cargo:rustc-env=TAURI_CONFIG={}" , inline) ;
292+ println ! ( "cargo:rustc-env=OPENVCS_APP_CHANNEL={}" , channel. slug) ;
199293
200294 // Also persist a copy alongside OUT_DIR for debugging (non-fatal if it fails)
201295 if let Ok ( out_dir) = env:: var ( "OUT_DIR" ) {
@@ -205,6 +299,8 @@ fn main() {
205299
206300 // Re-run if the base config changes
207301 println ! ( "cargo:rerun-if-changed={}" , base. display( ) ) ;
302+ let config_path = manifest_dir. join ( "../channel-metadata.json" ) ;
303+ println ! ( "cargo:rerun-if-changed={}" , config_path. display( ) ) ;
208304 println ! ( "cargo:rerun-if-env-changed=OPENVCS_UPDATE_CHANNEL" ) ;
209305 println ! ( "cargo:rerun-if-env-changed=OPENVCS_FLATPAK" ) ;
210306 println ! ( "cargo:rerun-if-env-changed=OPENVCS_OFFICIAL_RELEASE" ) ;
@@ -266,7 +362,7 @@ fn main() {
266362 pkg_version. clone ( )
267363 } else {
268364 let branch_ident = sanitize_semver_ident ( & branch) ;
269- let channel_suffix = match chan . as_str ( ) {
365+ let channel_suffix = match channel . slug {
270366 "beta" => "-beta" ,
271367 "nightly" => "-nightly" ,
272368 _ => "" ,
0 commit comments