Skip to content

Commit c81c197

Browse files
committed
Merge branch 'Dev' into Beta
2 parents 6114c6d + 7d75141 commit c81c197

18 files changed

Lines changed: 429 additions & 119 deletions

.github/workflows/beta.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ jobs:
105105
if (e.status !== 422) throw e;
106106
}
107107
108+
- name: Write channel Tauri config override
109+
env:
110+
OPENVCS_UPDATE_CHANNEL: beta
111+
run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json"
112+
108113
- name: Build and publish Beta prerelease
109114
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2
110115
env:
@@ -124,5 +129,4 @@ jobs:
124129
Runner: ${{ runner.os }} • Run #${{ github.run_number }}
125130
releaseDraft: true
126131
prerelease: true
127-
args: ${{ matrix.args }}
128-
132+
args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }}

.github/workflows/nightly.yml

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ permissions:
2020
env:
2121
# Scheduled runs should always build from Dev. Manual runs should build from the selected branch.
2222
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'Dev' }}
23-
# Flatpak is enabled in CI.
24-
OPENVCS_ENABLE_FLATPAK: ${{ vars.ENABLE_FLATPAK }}
2523

2624
jobs:
2725
check-changes:
@@ -187,42 +185,6 @@ jobs:
187185
sudo apt-get install -y libappindicator3-dev librsvg2-dev patchelf
188186
sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev
189187
190-
# ---------- Flatpak (Linux only; artifact) ----------
191-
- name: Install Flatpak tooling
192-
if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != ''
193-
run: |
194-
set -euxo pipefail
195-
sudo apt-get update
196-
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder appstream elfutils
197-
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
198-
flatpak install --user -y flathub org.gnome.Platform//49 org.gnome.Sdk//49
199-
# Cargo/rustc must come from the Flatpak SDK environment (not the host toolchain).
200-
# Try common branches to avoid coupling this workflow to the host runner image.
201-
flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 \
202-
|| flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//24.08 \
203-
|| flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable
204-
205-
- name: Verify Flatpak manifest exists
206-
if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != ''
207-
run: |
208-
set -euxo pipefail
209-
ls -la packaging/flatpak
210-
test -f packaging/flatpak/io.github.jordonbc.OpenVCS.yml
211-
212-
- name: Build Flatpak bundle
213-
if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != ''
214-
run: |
215-
set -euxo pipefail
216-
flatpak-builder --user --force-clean --repo=repo build-flatpak packaging/flatpak/io.github.jordonbc.OpenVCS.yml
217-
flatpak build-bundle repo OpenVCS.flatpak io.github.jordonbc.OpenVCS
218-
219-
- name: Upload Flatpak bundle (artifact)
220-
if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != ''
221-
uses: actions/upload-artifact@v7
222-
with:
223-
name: OpenVCS-flatpak-nightly
224-
path: OpenVCS.flatpak
225-
226188
# ---------- Cache ----------
227189
- name: Rust cache
228190
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
@@ -252,6 +214,11 @@ jobs:
252214
if (e.status !== 422) throw e;
253215
}
254216
217+
- name: Write channel Tauri config override
218+
env:
219+
OPENVCS_UPDATE_CHANNEL: nightly
220+
run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json"
221+
255222
# ---------- Build & publish ----------
256223
- name: Build and publish Nightly prerelease
257224
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2
@@ -278,10 +245,4 @@ jobs:
278245
${{ steps.meta.outputs.changelog }}
279246
releaseDraft: false
280247
prerelease: true
281-
args: ${{ matrix.args }}
282-
283-
- name: Upload Flatpak bundle to GitHub Release
284-
if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != ''
285-
env:
286-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
287-
run: gh release upload openvcs-nightly OpenVCS.flatpak --clobber
248+
args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }}

.github/workflows/publish-stable.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ jobs:
132132
- name: Cargo clippy
133133
run: cargo clippy --all-targets -- -D warnings
134134

135+
- name: Write channel Tauri config override
136+
env:
137+
OPENVCS_UPDATE_CHANNEL: stable
138+
run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json"
139+
135140
# ---------- Build & publish with Tauri action ----------
136141
- name: Build and create GitHub Release (draft)
137142
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2
@@ -155,7 +160,7 @@ jobs:
155160
releaseBody: 'See the assets to download this version and install.'
156161
releaseDraft: true
157162
prerelease: false
158-
args: ${{ matrix.args }}
163+
args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }}
159164
includeDebug: false # default; set true if you want debug archives too
160165
# bundles: '' # e.g. 'deb,appimage,msi,nsis' if you want to restrict output
161166

Backend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
1919

2020
[build-dependencies]
2121
tauri-build = { version = "2.4", default-features = false, features = [] }
22+
serde = { version = "1", features = ["derive"] }
2223
serde_json = "1.0"
2324

2425
[features]

Backend/build.rs

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,101 @@
11
// Copyright © 2025-2026 OpenVCS Contributors
22
// SPDX-License-Identifier: GPL-3.0-or-later
3+
use serde::Deserialize;
34
use 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.
6100
fn 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
_ => "",

Backend/src/app_identity.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright © 2025-2026 OpenVCS Contributors
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
//! Channel-aware desktop identity and persistence paths.
5+
6+
use directories::ProjectDirs;
7+
8+
/// Returns the filesystem app name used for persistence.
9+
///
10+
/// All desktop channels intentionally share the historical `OpenVCS`
11+
/// directory so builds keep using the same config and plugin roots.
12+
///
13+
/// # Returns
14+
/// - Application name for `ProjectDirs`.
15+
pub fn persistence_name() -> &'static str {
16+
"OpenVCS"
17+
}
18+
19+
/// Returns channel-aware project directories for app config and data.
20+
///
21+
/// All desktop channels preserve the legacy `OpenVCS` application name so
22+
/// existing users keep the same config and data roots.
23+
///
24+
/// # Returns
25+
/// - `Some(ProjectDirs)` when the platform exposes standard app directories.
26+
/// - `None` when no platform-specific directories are available.
27+
pub fn project_dirs() -> Option<ProjectDirs> {
28+
ProjectDirs::from("dev", "OpenVCS", persistence_name())
29+
}
30+
31+
#[cfg(test)]
32+
mod tests {
33+
use super::persistence_name;
34+
35+
#[test]
36+
fn exposes_persistence_names() {
37+
assert_eq!(persistence_name(), "OpenVCS");
38+
}
39+
}

Backend/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tauri_plugin_updater::UpdaterExt;
1414

1515
use crate::core::BackendId;
1616

17+
mod app_identity;
1718
mod core;
1819
mod logging;
1920
mod output_log;

Backend/src/plugin_paths.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: GPL-3.0-or-later
33
//! Path resolution helpers for installed and built-in plugins.
44
5-
use directories::ProjectDirs;
65
use log::{info, warn};
76
use std::{
87
env,
@@ -36,7 +35,7 @@ static LOGGED_BUILTIN_DIRS: AtomicBool = AtomicBool::new(false);
3635
/// # Returns
3736
/// - The absolute config-directory path used to store installed plugins.
3837
pub fn plugins_dir() -> PathBuf {
39-
if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") {
38+
if let Some(pd) = crate::app_identity::project_dirs() {
4039
pd.config_dir().join("plugins")
4140
} else {
4241
PathBuf::from("plugins")

Backend/src/plugin_runtime/settings_store.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33

44
//! Filesystem persistence for plugin settings JSON.
55
6-
use directories::ProjectDirs;
76
use serde_json::{Map, Value};
87
use std::fs;
98
use std::path::{Path, PathBuf};
109

1110
/// Returns the root plugin data directory under the app config directory.
1211
fn plugin_data_root() -> PathBuf {
13-
if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") {
12+
if let Some(pd) = crate::app_identity::project_dirs() {
1413
pd.config_dir().join("plugin-data")
1514
} else {
1615
PathBuf::from("plugin-data")

0 commit comments

Comments
 (0)