diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index cf2b8ae..0fbb878 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -105,6 +105,11 @@ jobs: if (e.status !== 422) throw e; } + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: beta + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + - name: Build and publish Beta prerelease uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 env: @@ -124,5 +129,4 @@ jobs: Runner: ${{ runner.os }} • Run #${{ github.run_number }} releaseDraft: true prerelease: true - args: ${{ matrix.args }} - + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2655b5e..a59add1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,8 +20,6 @@ permissions: env: # Scheduled runs should always build from Dev. Manual runs should build from the selected branch. TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || 'Dev' }} - # Flatpak is enabled in CI. - OPENVCS_ENABLE_FLATPAK: ${{ vars.ENABLE_FLATPAK }} jobs: check-changes: @@ -187,42 +185,6 @@ jobs: sudo apt-get install -y libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev - # ---------- Flatpak (Linux only; artifact) ---------- - - name: Install Flatpak tooling - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - sudo apt-get update - sudo apt-get install -y --no-install-recommends flatpak flatpak-builder appstream elfutils - flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak install --user -y flathub org.gnome.Platform//49 org.gnome.Sdk//49 - # Cargo/rustc must come from the Flatpak SDK environment (not the host toolchain). - # Try common branches to avoid coupling this workflow to the host runner image. - flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 \ - || flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable//24.08 \ - || flatpak install --user -y flathub org.freedesktop.Sdk.Extension.rust-stable - - - name: Verify Flatpak manifest exists - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - ls -la packaging/flatpak - test -f packaging/flatpak/io.github.jordonbc.OpenVCS.yml - - - name: Build Flatpak bundle - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - run: | - set -euxo pipefail - flatpak-builder --user --force-clean --repo=repo build-flatpak packaging/flatpak/io.github.jordonbc.OpenVCS.yml - flatpak build-bundle repo OpenVCS.flatpak io.github.jordonbc.OpenVCS - - - name: Upload Flatpak bundle (artifact) - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - uses: actions/upload-artifact@v7 - with: - name: OpenVCS-flatpak-nightly - path: OpenVCS.flatpak - # ---------- Cache ---------- - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -252,6 +214,11 @@ jobs: if (e.status !== 422) throw e; } + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: nightly + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + # ---------- Build & publish ---------- - name: Build and publish Nightly prerelease uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 @@ -278,10 +245,4 @@ jobs: ${{ steps.meta.outputs.changelog }} releaseDraft: false prerelease: true - args: ${{ matrix.args }} - - - name: Upload Flatpak bundle to GitHub Release - if: matrix.platform == 'ubuntu-24.04' && env.OPENVCS_ENABLE_FLATPAK == '1' && hashFiles('packaging/flatpak/io.github.jordonbc.OpenVCS.yml') != '' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload openvcs-nightly OpenVCS.flatpak --clobber + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} diff --git a/.github/workflows/publish-stable.yml b/.github/workflows/publish-stable.yml index fbf203e..b6639bc 100644 --- a/.github/workflows/publish-stable.yml +++ b/.github/workflows/publish-stable.yml @@ -132,6 +132,11 @@ jobs: - name: Cargo clippy run: cargo clippy --all-targets -- -D warnings + - name: Write channel Tauri config override + env: + OPENVCS_UPDATE_CHANNEL: stable + run: node scripts/write-tauri-channel-config.js "${{ runner.temp }}/tauri.channel.conf.json" + # ---------- Build & publish with Tauri action ---------- - name: Build and create GitHub Release (draft) uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # action-v0.6.2 @@ -155,7 +160,7 @@ jobs: releaseBody: 'See the assets to download this version and install.' releaseDraft: true prerelease: false - args: ${{ matrix.args }} + args: ${{ format('{0} --config {1}/tauri.channel.conf.json', matrix.args || '', runner.temp) }} includeDebug: false # default; set true if you want debug archives too # bundles: '' # e.g. 'deb,appimage,msi,nsis' if you want to restrict output diff --git a/Backend/Cargo.toml b/Backend/Cargo.toml index 7ae0ff8..abe1ff6 100644 --- a/Backend/Cargo.toml +++ b/Backend/Cargo.toml @@ -19,6 +19,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2.4", default-features = false, features = [] } +serde = { version = "1", features = ["derive"] } serde_json = "1.0" [features] diff --git a/Backend/build.rs b/Backend/build.rs index 21372f0..0cf32e8 100644 --- a/Backend/build.rs +++ b/Backend/build.rs @@ -1,7 +1,101 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later +use serde::Deserialize; use std::{env, fs, path::PathBuf, process::Command}; +fn load_channel_metadata() -> ChannelMetadata { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let config_path = manifest_dir.join("../channel-metadata.json"); + let data = fs::read_to_string(&config_path).expect("read channel-metadata.json"); + serde_json::from_str(&data).expect("parse channel-metadata.json") +} + +#[derive(Clone, Debug, Deserialize)] +struct ChannelMetadata { + channels: Channels, +} + +#[derive(Clone, Debug, Deserialize)] +struct Channels { + stable: ChannelEntry, + beta: ChannelEntry, + nightly: ChannelEntry, +} + +#[derive(Clone, Debug, Deserialize)] +struct ChannelEntry { + slug: String, + #[serde(rename = "mainBinaryName")] + main_binary_name: String, + #[serde(rename = "productName")] + product_name: String, + identifier: String, + #[serde(rename = "windowTitle")] + window_title: String, + #[serde(rename = "updaterEndpoints")] + updater_endpoints: Vec, +} + +/// Channel-specific metadata used for generated desktop bundles. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ChannelConfig { + /// Normalized channel slug used across build and runtime. + slug: &'static str, + /// Binary and bundle stem without spaces. + main_binary_name: &'static str, + /// Human-facing desktop product name. + product_name: &'static str, + /// Tauri bundle identifier. + identifier: &'static str, + /// Main window title. + window_title: &'static str, + /// Updater endpoint URLs. + updater_endpoints: &'static [&'static str], +} + +impl ChannelConfig { + /// Resolves normalized channel metadata from an arbitrary environment value. + /// + /// # Parameters + /// - `raw`: Raw environment value. + /// + /// # Returns + /// - Stable metadata when the input is missing or unknown. + /// - Beta or nightly metadata for recognized channel names. + fn from_env_value(raw: &str, metadata: &ChannelMetadata) -> Self { + let slug = raw.trim().to_ascii_lowercase(); + let entry = match slug.as_str() { + "beta" => &metadata.channels.beta, + "nightly" => &metadata.channels.nightly, + _ => { + if !raw.trim().is_empty() { + eprintln!("Warning: Unknown channel '{raw}', defaulting to stable"); + } + &metadata.channels.stable + } + }; + Self::from_entry(entry) + } + + fn from_entry(entry: &ChannelEntry) -> Self { + let endpoints: Vec<&'static str> = entry + .updater_endpoints + .iter() + .map(|s| Box::leak(s.to_string().into_boxed_str()) as &str) + .collect(); + Self { + // Box::leak is acceptable here - this is a build script that runs once. + // Memory is leaked for the program duration but that's fine for a build script. + slug: Box::leak(entry.slug.clone().into_boxed_str()), + main_binary_name: Box::leak(entry.main_binary_name.clone().into_boxed_str()), + product_name: Box::leak(entry.product_name.clone().into_boxed_str()), + identifier: Box::leak(entry.identifier.clone().into_boxed_str()), + window_title: Box::leak(entry.window_title.clone().into_boxed_str()), + updater_endpoints: Box::leak(endpoints.into_boxed_slice()), + } + } +} + /// Returns whether the build is running for Flatpak packaging. fn is_flatpak_build() -> bool { matches!( @@ -131,37 +225,36 @@ fn main() { let data = fs::read_to_string(&base).expect("read tauri.conf.json"); let mut json: serde_json::Value = serde_json::from_str(&data).expect("parse tauri.conf.json"); - // Compute channel based on environment; default to stable - let chan = env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()); - - // Repository URL (can be overridden via env var for forks) - let repo = - env::var("OPENVCS_REPO").unwrap_or_else(|_| "https://github.com/Jordonbc/OpenVCS".into()); - - // Build update URLs from repository - let stable = - serde_json::Value::String(format!("{}/releases/latest/download/latest.json", repo)); - let beta = serde_json::Value::String(format!( - "{}/releases/download/openvcs-beta/latest.json", - repo - )); - let nightly = serde_json::Value::String(format!( - "{}/releases/download/openvcs-nightly/latest.json", - repo - )); + // Compute channel based on environment; default to stable. + let channel_metadata = load_channel_metadata(); + let channel = ChannelConfig::from_env_value( + &env::var("OPENVCS_UPDATE_CHANNEL").unwrap_or_else(|_| "stable".into()), + &channel_metadata, + ); // Navigate: plugins.updater.endpoints if let Some(plugins) = json.get_mut("plugins") { if let Some(updater) = plugins.get_mut("updater") { - let endpoints = match chan.as_str() { - // Beta: check beta first, then stable - "beta" => serde_json::Value::Array(vec![beta.clone(), stable.clone()]), - // Nightly: check nightly first, then stable - "nightly" => serde_json::Value::Array(vec![nightly.clone(), stable.clone()]), - // Stable: stable only - _ => serde_json::Value::Array(vec![stable.clone()]), - }; - updater["endpoints"] = endpoints; + let endpoints: Vec = channel + .updater_endpoints + .iter() + .map(|s| serde_json::Value::String((*s).to_string())) + .collect(); + updater["endpoints"] = serde_json::Value::Array(endpoints); + } + } + + json["mainBinaryName"] = serde_json::Value::String(channel.main_binary_name.into()); + json["productName"] = serde_json::Value::String(channel.product_name.into()); + json["identifier"] = serde_json::Value::String(channel.identifier.into()); + if let Some(app) = json.get_mut("app") { + if let Some(windows) = app + .get_mut("windows") + .and_then(|value| value.as_array_mut()) + { + if let Some(main_window) = windows.first_mut() { + main_window["title"] = serde_json::Value::String(channel.window_title.into()); + } } } @@ -196,6 +289,7 @@ fn main() { // Provide the generated config via inline JSON env var (must be single-line) let inline = serde_json::to_string(&json).unwrap(); println!("cargo:rustc-env=TAURI_CONFIG={}", inline); + println!("cargo:rustc-env=OPENVCS_APP_CHANNEL={}", channel.slug); // Also persist a copy alongside OUT_DIR for debugging (non-fatal if it fails) if let Ok(out_dir) = env::var("OUT_DIR") { @@ -205,6 +299,8 @@ fn main() { // Re-run if the base config changes println!("cargo:rerun-if-changed={}", base.display()); + let config_path = manifest_dir.join("../channel-metadata.json"); + println!("cargo:rerun-if-changed={}", config_path.display()); println!("cargo:rerun-if-env-changed=OPENVCS_UPDATE_CHANNEL"); println!("cargo:rerun-if-env-changed=OPENVCS_FLATPAK"); println!("cargo:rerun-if-env-changed=OPENVCS_OFFICIAL_RELEASE"); @@ -266,7 +362,7 @@ fn main() { pkg_version.clone() } else { let branch_ident = sanitize_semver_ident(&branch); - let channel_suffix = match chan.as_str() { + let channel_suffix = match channel.slug { "beta" => "-beta", "nightly" => "-nightly", _ => "", diff --git a/Backend/src/app_identity.rs b/Backend/src/app_identity.rs new file mode 100644 index 0000000..c0eb545 --- /dev/null +++ b/Backend/src/app_identity.rs @@ -0,0 +1,39 @@ +// Copyright © 2025-2026 OpenVCS Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Channel-aware desktop identity and persistence paths. + +use directories::ProjectDirs; + +/// Returns the filesystem app name used for persistence. +/// +/// All desktop channels intentionally share the historical `OpenVCS` +/// directory so builds keep using the same config and plugin roots. +/// +/// # Returns +/// - Application name for `ProjectDirs`. +pub fn persistence_name() -> &'static str { + "OpenVCS" +} + +/// Returns channel-aware project directories for app config and data. +/// +/// All desktop channels preserve the legacy `OpenVCS` application name so +/// existing users keep the same config and data roots. +/// +/// # Returns +/// - `Some(ProjectDirs)` when the platform exposes standard app directories. +/// - `None` when no platform-specific directories are available. +pub fn project_dirs() -> Option { + ProjectDirs::from("dev", "OpenVCS", persistence_name()) +} + +#[cfg(test)] +mod tests { + use super::persistence_name; + + #[test] + fn exposes_persistence_names() { + assert_eq!(persistence_name(), "OpenVCS"); + } +} diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 475422d..6112003 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -14,6 +14,7 @@ use tauri_plugin_updater::UpdaterExt; use crate::core::BackendId; +mod app_identity; mod core; mod logging; mod output_log; diff --git a/Backend/src/plugin_paths.rs b/Backend/src/plugin_paths.rs index acf25bd..d6152b1 100644 --- a/Backend/src/plugin_paths.rs +++ b/Backend/src/plugin_paths.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Path resolution helpers for installed and built-in plugins. -use directories::ProjectDirs; use log::{info, warn}; use std::{ env, @@ -36,7 +35,7 @@ static LOGGED_BUILTIN_DIRS: AtomicBool = AtomicBool::new(false); /// # Returns /// - The absolute config-directory path used to store installed plugins. pub fn plugins_dir() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("plugins") } else { PathBuf::from("plugins") diff --git a/Backend/src/plugin_runtime/settings_store.rs b/Backend/src/plugin_runtime/settings_store.rs index deebc45..605e045 100644 --- a/Backend/src/plugin_runtime/settings_store.rs +++ b/Backend/src/plugin_runtime/settings_store.rs @@ -3,14 +3,13 @@ //! Filesystem persistence for plugin settings JSON. -use directories::ProjectDirs; use serde_json::{Map, Value}; use std::fs; use std::path::{Path, PathBuf}; /// Returns the root plugin data directory under the app config directory. fn plugin_data_root() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("plugin-data") } else { PathBuf::from("plugin-data") diff --git a/Backend/src/settings.rs b/Backend/src/settings.rs index 2dd721a..b9390f8 100644 --- a/Backend/src/settings.rs +++ b/Backend/src/settings.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later //! Global application configuration types and persistence helpers. -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::{fs, io}; @@ -669,7 +668,7 @@ impl AppConfig { /// # Returns /// - Filesystem path to the global OpenVCS config file. pub fn path() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.config_dir().join("openvcs.conf") } else { PathBuf::from("openvcs.conf") diff --git a/Backend/src/state.rs b/Backend/src/state.rs index e475097..46452e9 100644 --- a/Backend/src/state.rs +++ b/Backend/src/state.rs @@ -13,7 +13,6 @@ use crate::plugin_runtime::PluginRuntimeManager; use crate::repo::Repo; use crate::repo_settings::RepoConfig; use crate::settings::AppConfig; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; /// Default number of recent repositories stored when settings are missing or invalid. @@ -287,7 +286,7 @@ struct RecentFileEntry { /// # Returns /// - Recents file path. fn recents_file_path() -> PathBuf { - if let Some(pd) = ProjectDirs::from("dev", "OpenVCS", "OpenVCS") { + if let Some(pd) = crate::app_identity::project_dirs() { pd.data_dir().join("recents.json") } else { PathBuf::from("recents.json") diff --git a/Justfile b/Justfile index edd2005..9d13e97 100644 --- a/Justfile +++ b/Justfile @@ -1,17 +1,26 @@ default: @just --list -build target="all": - @just {{ if target == "plugins" { "_build_plugins" } else if target == "client" { "_build_client" } else if target == "all" { "_build_all" } else { "_build_usage" } }} - -_build_all: _build_client - -_build_client: +build target="stable": + @case "{{target}}" in \ + plugins) just _build_plugins ;; \ + client|stable|beta|nightly) just _build_client "{{target}}" ;; \ + all) just _build_all stable ;; \ + *) just _build_usage ;; \ + esac + +_build_all channel="stable": + @just _build_client {{channel}} + +_build_client channel="stable": npm --prefix Frontend run build - cargo build + FRONTEND_SKIP_BUILD=1 NO_STRIP=true OPENVCS_UPDATE_CHANNEL={{channel}} node scripts/tauri-build.js + +_build_plugins: + @echo "Plugin-only builds are not currently implemented by this Justfile" _build_usage: - @echo "Unknown build target. Use: just build, just build plugins, or just build client" + @echo "Unknown build target. Use: just build, just build stable, just build beta, just build nightly, just build client, or just build plugins" @exit 2 test: @@ -19,8 +28,8 @@ test: cd Frontend && npm exec tsc -- -p tsconfig.json --noEmit || true cd Frontend && npx vitest run || true -tauri-build: - node scripts/tauri-build.js +tauri-build channel="stable": + FRONTEND_SKIP_BUILD=1 NO_STRIP=true OPENVCS_UPDATE_CHANNEL={{channel}} node scripts/tauri-build.js build-flatpak install="": @if [ "{{install}}" = "-i" ]; then \ diff --git a/README.md b/README.md index f096c10..f3a4bae 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ curl -fsSL https://raw.githubusercontent.com/Jordonbc/OpenVCS/stable/install.sh The script targets Linux, leaves existing configuration untouched, and can be re-run to pull the newest release. +Desktop builds keep using the legacy shared `OpenVCS` config and plugin directories. + **Install pre-release (nightly):** ```bash @@ -42,6 +44,8 @@ curl -fsSL https://raw.githubusercontent.com/Jordonbc/OpenVCS/stable/install.sh Swap `stable` for `dev` in the URL if you want the bleeding-edge installer. +Pre-release AppImage installs use a separate launcher and install path when the selected release is branded as beta or nightly, so they can coexist with stable on the same machine. + --- ## Key Goals @@ -159,11 +163,15 @@ cargo tauri dev ```bash just tauri-build +just tauri-build beta ``` This wraps `cargo tauri build` with `NO_STRIP=true` to avoid AppImage linuxdeploy strip failures on newer Linux toolchains. +`just build`, `just build stable`, `just build beta`, and `just build nightly` +use the same channel-aware Tauri build flow. + The Tauri precommands in `Backend/tauri.conf.json` intentionally set an explicit hook `cwd` to `Backend/` so Tauri does not resolve them from nested plugin directories, and built-in plugins are bundled through each plugin's own diff --git a/channel-metadata.json b/channel-metadata.json new file mode 100644 index 0000000..4e053ff --- /dev/null +++ b/channel-metadata.json @@ -0,0 +1,36 @@ +{ + "repo": "https://github.com/Jordonbc/OpenVCS", + "channels": { + "stable": { + "slug": "stable", + "mainBinaryName": "openvcs", + "productName": "OpenVCS", + "identifier": "dev.jordon.openvcs", + "windowTitle": "OpenVCS", + "updaterEndpoints": ["https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json"] + }, + "beta": { + "slug": "beta", + "mainBinaryName": "openvcs-beta", + "productName": "OpenVCS-Beta", + "identifier": "dev.jordon.openvcs.beta", + "windowTitle": "OpenVCS Beta", + "updaterEndpoints": [ + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json" + ] + }, + "nightly": { + "slug": "nightly", + "mainBinaryName": "openvcs-nightly", + "productName": "OpenVCS-Nightly", + "identifier": "dev.jordon.openvcs.nightly", + "windowTitle": "OpenVCS Nightly", + "updaterEndpoints": [ + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-nightly/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/download/openvcs-beta/latest.json", + "https://github.com/Jordonbc/OpenVCS/releases/latest/download/latest.json" + ] + } + } +} diff --git a/install.sh b/install.sh index f0c4823..8390496 100755 --- a/install.sh +++ b/install.sh @@ -13,11 +13,7 @@ REPO_OWNER="Jordonbc" REPO_NAME="OpenVCS" INSTALL_DIR="${HOME}/Applications" -TARGET_BASENAME="openvcs.AppImage" -TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" - DESKTOP_DIR="${HOME}/.local/share/applications" -DESKTOP_PATH="${DESKTOP_DIR}/openvcs.desktop" ICON_NAME="openvcs" ICON_SOURCE_PATH="docs/images/logos/OpenVCS-256.png" ICON_URL_BRANCH="Dev" @@ -25,6 +21,13 @@ ICON_THEME_DIR="${HOME}/.local/share/icons/hicolor" ICON_TARGET_DIR="${ICON_THEME_DIR}/256x256/apps" ICON_PATH="${ICON_TARGET_DIR}/${ICON_NAME}.png" +APP_VARIANT="stable" +APP_DISPLAY_NAME="OpenVCS" +TARGET_BASENAME="openvcs.AppImage" +DESKTOP_BASENAME="openvcs.desktop" +TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" +DESKTOP_PATH="${DESKTOP_DIR}/${DESKTOP_BASENAME}" + # --- State --- INCLUDE_PRERELEASE=false UNINSTALL=false @@ -104,6 +107,64 @@ refresh_desktop_entries() { fi } +set_install_variant() { # $1: stable|beta|nightly|prerelease + case "$1" in + beta) + APP_VARIANT="beta" + APP_DISPLAY_NAME="OpenVCS Beta" + TARGET_BASENAME="openvcs-beta.AppImage" + DESKTOP_BASENAME="openvcs-beta.desktop" + ;; + nightly) + APP_VARIANT="nightly" + APP_DISPLAY_NAME="OpenVCS Nightly" + TARGET_BASENAME="openvcs-nightly.AppImage" + DESKTOP_BASENAME="openvcs-nightly.desktop" + ;; + prerelease) + APP_VARIANT="prerelease" + APP_DISPLAY_NAME="OpenVCS Pre-release" + TARGET_BASENAME="openvcs-prerelease.AppImage" + DESKTOP_BASENAME="openvcs-prerelease.desktop" + ;; + *) + APP_VARIANT="stable" + APP_DISPLAY_NAME="OpenVCS" + TARGET_BASENAME="openvcs.AppImage" + DESKTOP_BASENAME="openvcs.desktop" + ;; + esac + + TARGET_PATH="${INSTALL_DIR%/}/${TARGET_BASENAME}" + DESKTOP_PATH="${DESKTOP_DIR}/${DESKTOP_BASENAME}" +} + +detect_release_variant() { # $1: release tag, $2: asset name + local combined + combined="${1,,} ${2,,}" + + case "$combined" in + *nightly*|*openvcs-nightly*) printf 'nightly' ;; + *beta*|*openvcs-beta*) printf 'beta' ;; + *) + if $INCLUDE_PRERELEASE; then + printf 'prerelease' + else + printf 'stable' + fi + ;; + esac +} + +remove_file_if_present() { # $1: path, $2: label + if [[ -f "$1" ]]; then + echo "Removing $2: $1" + rm "$1" + else + echo "No $2 found at $1" + fi +} + # --- Parse flags --- for arg in "${@:-}"; do case "$arg" in @@ -190,20 +251,12 @@ trap 'if $INTERACTIVE_MODE; then show_error "Installation failed. Check network # --- Uninstall mode --- if $UNINSTALL; then - echo "Uninstalling OpenVCS..." - if [[ -f "${TARGET_PATH}" ]]; then - echo "Removing AppImage: ${TARGET_PATH}" - rm "${TARGET_PATH}" - else - echo "No AppImage found at ${TARGET_PATH}" - fi - - if [[ -f "${DESKTOP_PATH}" ]]; then - echo "Removing desktop entry: ${DESKTOP_PATH}" - rm "${DESKTOP_PATH}" - else - echo "No desktop entry found at ${DESKTOP_PATH}" - fi + echo "Uninstalling OpenVCS variants..." + for variant in stable beta nightly prerelease; do + set_install_variant "$variant" + remove_file_if_present "${TARGET_PATH}" "${APP_DISPLAY_NAME} AppImage" + remove_file_if_present "${DESKTOP_PATH}" "${APP_DISPLAY_NAME} desktop entry" + done if [[ -f "${ICON_PATH}" ]]; then echo "Removing icon: ${ICON_PATH}" @@ -217,9 +270,9 @@ if $UNINSTALL; then refresh_desktop_entries - echo "✅ OpenVCS uninstalled." + echo "✅ OpenVCS variants uninstalled." if $INTERACTIVE_MODE; then - show_info "✅ OpenVCS was uninstalled." + show_info "✅ OpenVCS variants were uninstalled." fi exit 0 fi @@ -294,6 +347,9 @@ fi echo "Selected release tag: ${RELEASE_TAG:-unknown}" $INCLUDE_PRERELEASE && echo "(including pre-releases)" +set_install_variant "$(detect_release_variant "${RELEASE_TAG:-}" "${ASSET_NAME:-}")" +echo "Installing variant: ${APP_DISPLAY_NAME}" + # --- Download safely (atomic on same filesystem) --- TMP_FILE="$(mktemp --tmpdir="${INSTALL_DIR}" ".openvcs.XXXXXXXX")" trap '[[ -f "${TMP_FILE:-}" ]] && rm "${TMP_FILE}"' EXIT @@ -314,7 +370,7 @@ echo "Writing desktop entry: ${DESKTOP_PATH}" cat > "${DESKTOP_PATH}" < { + try { + // channelConfigPath is /tmp/openvcs-tauri-config-XXXXXX/tauri.channel.conf.json + // dirname gives us /tmp/openvcs-tauri-config-XXXXXX which we then remove + fs.rmSync(path.dirname(channelConfigPath), { recursive: true, force: true }); + } catch {} if (signal) process.kill(process.pid, signal); process.exit(code ?? 1); }); diff --git a/scripts/write-tauri-channel-config.js b/scripts/write-tauri-channel-config.js new file mode 100644 index 0000000..ab676db --- /dev/null +++ b/scripts/write-tauri-channel-config.js @@ -0,0 +1,77 @@ +// Copyright © 2025-2026 OpenVCS Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +const fs = require('fs'); +const path = require('path'); + +const CHANNEL_METADATA_PATH = path.resolve(__dirname, '../channel-metadata.json'); + +function loadChannelMetadata() { + const data = fs.readFileSync(CHANNEL_METADATA_PATH, 'utf8'); + return JSON.parse(data); +} + +function resolveChannelConfig(raw) { + const metadata = loadChannelMetadata(); + const slug = (raw || 'stable').trim().toLowerCase(); + + let entry = metadata.channels[slug]; + if (!entry) { + if (raw && raw.trim()) { + console.warn(`Warning: Unknown channel '${raw}', defaulting to stable`); + } + entry = metadata.channels.stable; + } + + return { + mainBinaryName: entry.mainBinaryName, + productName: entry.productName, + identifier: entry.identifier, + windowTitle: entry.windowTitle, + updaterEndpoints: entry.updaterEndpoints, + }; +} + +function writeChannelConfig(outputPath) { + const channel = resolveChannelConfig(process.env.OPENVCS_UPDATE_CHANNEL); + const override = { + mainBinaryName: channel.mainBinaryName, + productName: channel.productName, + identifier: channel.identifier, + app: { + windows: [ + { + title: channel.windowTitle, + }, + ], + }, + plugins: { + updater: { + endpoints: channel.updaterEndpoints, + }, + }, + }; + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(override, null, 2)); +} + +function main() { + const outputPath = process.argv[2]; + if (!outputPath) { + console.error('Usage: node scripts/write-tauri-channel-config.js '); + process.exit(2); + } + + writeChannelConfig(path.resolve(outputPath)); +} + +module.exports = { + writeChannelConfig, + resolveChannelConfig, + loadChannelMetadata, +}; + +if (require.main === module) { + main(); +}