4 Commits
Author SHA1 Message Date
reya 584ab34df6 chore: release version 1.0.1 2026-09-07 13:58:43 +07:00
reya 0514c1d982 fix: panic on flatpak installations 2026-09-07 13:49:36 +07:00
reya 2f834a0bcc chore: update deps 2026-08-31 16:54:38 +07:00
reya 49cd5cb9a0 chore: clean up 2026-08-03 09:53:12 +07:00
21 changed files with 663 additions and 827 deletions
Generated
+480 -648
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "desktop", "web"]
default-members = ["desktop"] default-members = ["desktop"]
[workspace.package] [workspace.package]
version = "1.0.0-beta5" version = "1.0.1"
edition = "2024" edition = "2024"
publish = false publish = false
-62
View File
@@ -1,62 +0,0 @@
[workspace]
resolver = "2"
members = ["crates/*", "desktop", "web"]
default-members = ["desktop"]
[workspace.package]
version = "1.0.0-beta5"
edition = "2024"
publish = false
[workspace.dependencies]
# GPUI
gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] }
# Others
anyhow = "1.0.44"
chrono = { version = "0.4.38", features = ["wasmbind"] }
futures = "0.3"
itertools = "0.13.0"
log = "0.4"
oneshot = "0.1.10"
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
rust-embed = { version = "8.5", features = ["include-exclude"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = "1"
smallvec = "1.14.0"
smol = "2"
webbrowser = "1.0.4"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
errno = { version = "0.3.14", default-features = false }
instant = "0.1"
[patch.crates-io]
# Use stacker's psm version which may have better WASM support
psm = { git = "https://github.com/rust-lang/stacker", branch = "master" }
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
[profile.profiling]
inherits = "release"
debug = true
+36 -6
View File
@@ -5,6 +5,7 @@ use gpui_updater::{EngineConfig, GitHubSource, UpdateStatus, Updater, Version};
use instant::{Duration, Instant}; use instant::{Duration, Instant};
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION"; const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
const COOP_BUNDLE_TYPE: &str = "COOP_BUNDLE_TYPE";
fn get_github_repo_owner() -> String { fn get_github_repo_owner() -> String {
std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "reyakov".to_string()) std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "reyakov".to_string())
@@ -14,18 +15,27 @@ fn get_github_repo_name() -> String {
std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "coop".to_string()) std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "coop".to_string())
} }
fn is_flatpak_installation() -> bool { /// Whether updates are managed by an external distribution channel
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok() /// (Flatpak/Snap), in which case the in-app updater must not run.
fn uses_managed_updates() -> bool {
// The Flatpak runtime exports `FLATPAK_ID` inside the sandbox.
std::env::var("FLATPAK_ID").is_ok()
// Allow opting out of in-app updates via an explicit environment variable.
|| std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
// The Snap package sets `COOP_BUNDLE_TYPE=snap` (see snapcraft.yaml.in).
|| std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value| value == "snap")
} }
/// Initialize the auto-update system. /// Initialize the auto-update system.
/// ///
/// Skips initialization when running as a Flatpak (updates are handled by the /// Skips initialization when updates are handled by an external distribution
/// Flatpak distribution channel). Otherwise creates the global [`AutoUpdater`] /// channel (Flatpak/Snap). Otherwise creates the global [`AutoUpdater`]
/// entity and schedules a check for updates after a 2-minute delay. /// entity and schedules a check for updates after a 2-minute delay.
pub fn init(window: &mut Window, cx: &mut App) { pub fn init(window: &mut Window, cx: &mut App) {
if is_flatpak_installation() { if uses_managed_updates() {
log::info!("Skipping auto-update initialization: App is installed via Flatpak"); log::info!(
"Skipping auto-update initialization: App is installed via a managed distribution channel (Flatpak/Snap)"
);
return; return;
} }
@@ -57,7 +67,27 @@ pub struct AutoUpdater {
} }
impl AutoUpdater { impl AutoUpdater {
/// Whether auto-update is available for this installation.
///
/// Returns `false` on managed distribution channels (Flatpak/Snap), where
/// updates are handled by the channel and no global updater is created.
pub fn is_available(cx: &App) -> bool {
cx.try_global::<GlobalAutoUpdater>().is_some()
}
/// Retrieve the global auto updater instance, if one was initialized.
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalAutoUpdater>()
.map(|global| global.0.clone())
}
/// Retrieve the global auto updater instance. /// Retrieve the global auto updater instance.
///
/// # Panics
///
/// Panics when auto-update is not available for this installation. Prefer
/// [`AutoUpdater::try_global`] when the installation type is not known at
/// compile time (e.g. Flatpak/Snap).
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAutoUpdater>().0.clone() cx.global::<GlobalAutoUpdater>().0.clone()
} }
+1 -1
View File
@@ -11,7 +11,7 @@ publish = false
atomic-destructor = "0.2" atomic-destructor = "0.2"
event-listener = "5" event-listener = "5"
nostr.workspace = true nostr.workspace = true
opaquerr = "0.1" opaquerr = { version = "0.1", features = ["alloc"] }
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
smol.workspace = true smol.workspace = true
+19 -6
View File
@@ -365,7 +365,8 @@ impl ChatRegistry {
.query(filter) .query(filter)
.await .await
.unwrap_or_default() .unwrap_or_default()
.first_owned() .into_iter()
.next()
.is_some(); .is_some();
if !found { if !found {
@@ -397,7 +398,8 @@ impl ChatRegistry {
.database() .database()
.query(filter) .query(filter)
.await? .await?
.first_owned() .into_iter()
.next()
.ok_or(anyhow::anyhow!("No inbox relays found"))?; .ok_or(anyhow::anyhow!("No inbox relays found"))?;
let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect(); let relays: Vec<RelayUrl> = nip17::extract_relay_list(&event).collect();
@@ -656,15 +658,26 @@ impl ChatRegistry {
cx.background_spawn(async move { cx.background_spawn(async move {
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
let contacts = client
// Query the latest contact list (previously `NostrDatabaseExt::contacts_public_keys`)
let filter = Filter::new()
.author(public_key)
.kind(Kind::ContactList)
.limit(1);
let contacts: HashSet<PublicKey> = client
.database() .database()
.contacts_public_keys(public_key) .query(filter)
.await .await
.unwrap_or_default()
.into_iter()
.next()
.map(|event| event.tags.public_keys().collect())
.unwrap_or_default(); .unwrap_or_default();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14"); .custom_tag(SingleLetterTag::LOWERCASE_K, "14");
let events = client.database().query(filter).await?; let events = client.database().query(filter).await?;
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new(); let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
@@ -827,7 +840,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul
async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> { async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
let filter = Filter::new().identifier(gift_wrap).limit(1); let filter = Filter::new().identifier(gift_wrap).limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = client.database().query(filter).await?.into_iter().next() {
UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e)) UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
} else { } else {
Err(anyhow!("Event is not cached yet.")) Err(anyhow!("Event is not cached yet."))
+5 -8
View File
@@ -403,7 +403,7 @@ impl Room {
cx.background_spawn(async move { cx.background_spawn(async move {
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::R), room_id); .custom_tag(SingleLetterTag::LOWERCASE_R, room_id);
let messages = client let messages = client
.database() .database()
@@ -461,13 +461,10 @@ impl Room {
// Add all receiver tags (no intermediate allocation) // Add all receiver tags (no intermediate allocation)
for public_key in self.members.iter().filter(|pk| *pk != &sender) { for public_key in self.members.iter().filter(|pk| *pk != &sender) {
let member = persons.read(cx).get(public_key, cx); let member = persons.read(cx).get(public_key, cx);
tags.push( tags.push(Tag::from(Nip01Tag::PublicKey {
Nip01Tag::PublicKey { public_key: member.public_key(),
public_key: member.public_key(), relay_hint: member.messaging_relay_hint(),
relay_hint: member.messaging_relay_hint(), }));
}
.to_tag(),
);
} }
// Construct a direct message rumor event // Construct a direct message rumor event
+2 -2
View File
@@ -4,13 +4,13 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use instant::Duration;
use anyhow::{Context as AnyhowContext, Error, anyhow}; use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::{ use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement, App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
SharedString, Styled, Subscription, Task, Window, div, relative, SharedString, Styled, Subscription, Task, Window, div, relative,
}; };
use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use settings::AppSettings; use settings::AppSettings;
@@ -414,7 +414,7 @@ impl DeviceRegistry {
.pubkey(app_pubkey) .pubkey(app_pubkey)
.limit(1); .limit(1);
match client.database().query(filter).await?.first_owned() { match client.database().query(filter).await?.into_iter().next() {
// Found an approval event // Found an approval event
Some(event) => Ok(Some(event)), Some(event) => Ok(Some(event)),
// No approval event found, construct a request event // No approval event found, construct a request event
+16 -5
View File
@@ -84,8 +84,20 @@ impl Screening {
let task: Task<Result<bool, Error>> = cx.background_spawn(async move { let task: Task<Result<bool, Error>> = cx.background_spawn(async move {
// Check if user is in contact list // Check if user is in contact list
let contacts = client.database().contacts_public_keys(current_user).await; let filter = Filter::new()
let followed = contacts.unwrap_or_default().contains(&public_key); .author(current_user)
.kind(Kind::ContactList)
.limit(1);
let followed = client
.database()
.query(filter)
.await
.unwrap_or_default()
.into_iter()
.next()
.map(|event| event.tags.public_keys().any(|k| k == public_key))
.unwrap_or(false);
Ok(followed) Ok(followed)
}); });
@@ -228,11 +240,10 @@ impl Screening {
let public_key = self.public_key; let public_key = self.public_key;
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let tag = Nip56Tag::PublicKey { let tag = Tag::from(Nip56Tag::PublicKey {
public_key, public_key,
report: Report::Impersonation, report: Report::Impersonation,
} });
.to_tag();
let event = EventBuilder::new(Kind::Reporting, "") let event = EventBuilder::new(Kind::Reporting, "")
.tag(tag) .tag(tag)
+27 -16
View File
@@ -380,12 +380,15 @@ impl Workspace {
self.import_encryption(window, cx); self.import_encryption(window, cx);
} }
Command::Update => { Command::Update => {
let auto_updater = AutoUpdater::global(cx); // No-op on managed distribution channels (Flatpak/Snap) where
auto_updater.update(cx, |this, cx| { // the in-app updater is never initialized.
this.updater.update(cx, |updater, cx| { if let Some(auto_updater) = AutoUpdater::try_global(cx) {
updater.check(cx); auto_updater.update(cx, |this, cx| {
this.updater.update(cx, |updater, cx| {
updater.check(cx);
});
}); });
}); }
} }
} }
} }
@@ -559,7 +562,7 @@ impl Workspace {
.caret() .caret()
.compact() .compact()
.transparent() .transparent()
.dropdown_menu(move |this, _window, _cx| { .dropdown_menu(move |this, _window, cx| {
let avatar = avatar.clone(); let avatar = avatar.clone();
let name = name.clone(); let name = name.clone();
@@ -593,12 +596,15 @@ impl Workspace {
IconName::Sun, IconName::Sun,
Box::new(Command::ToggleTheme), Box::new(Command::ToggleTheme),
) )
.separator() // Only offer in-app updates when auto-update is
.menu_with_icon( // enabled (managed channels update themselves).
"Check for Updates", .when(AutoUpdater::is_available(cx), |this| {
IconName::Device, this.separator().menu_with_icon(
Box::new(Command::Update), "Check for Updates",
) IconName::Device,
Box::new(Command::Update),
)
})
.menu_with_icon( .menu_with_icon(
"Settings", "Settings",
IconName::Settings, IconName::Settings,
@@ -610,7 +616,6 @@ impl Workspace {
} }
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement { fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let updater = AutoUpdater::global(cx);
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx); let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
@@ -622,13 +627,19 @@ impl Workspace {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx); let profile = persons.read(cx).get(&public_key, cx);
let announcement = profile.announcement(); let announcement = profile.announcement();
let updater_idle = updater.read(cx).idle(cx);
// Update status is only shown when auto-update is available. On
// managed distribution channels (Flatpak/Snap) no updater exists, so
// nothing is rendered.
let updater_status = AutoUpdater::try_global(cx).and_then(|updater| {
let updater = updater.read(cx);
(!updater.idle(cx)).then(|| updater.status(cx))
});
h_flex() h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2() .gap_2()
.when(!updater_idle, |this| { .when_some(updater_status, |this, status| {
let status = updater.read(cx).status(cx);
this.child(div().text_xs().italic().child(status)) this.child(div().text_xs().italic().child(status))
}) })
.when(nip4e_enabled, |this| { .when(nip4e_enabled, |this| {
+14 -1
View File
@@ -88,7 +88,20 @@ impl ContactListPanel {
}; };
let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move { let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move {
let contact_list = client.database().contacts_public_keys(public_key).await?; let filter = Filter::new()
.author(public_key)
.kind(Kind::ContactList)
.limit(1);
let contact_list: HashSet<PublicKey> = client
.database()
.query(filter)
.await?
.into_iter()
.next()
.map(|event| event.tags.public_keys().collect())
.unwrap_or_default();
Ok(contact_list) Ok(contact_list)
}); });
@@ -93,7 +93,7 @@ impl MessagingRelayPanel {
.author(public_key) .author(public_key)
.limit(1); .limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = client.database().query(filter).await?.into_iter().next() {
Ok(nip17::extract_relay_list(&event).collect()) Ok(nip17::extract_relay_list(&event).collect())
} else { } else {
Err(anyhow!("Not found.")) Err(anyhow!("Not found."))
@@ -177,7 +177,7 @@ impl MessagingRelayPanel {
let tags: Vec<Tag> = self let tags: Vec<Tag> = self
.relays .relays
.iter() .iter()
.map(|relay| Nip17Tag::Relay(relay.to_owned()).to_tag()) .map(|relay| Tag::from(Nip17Tag::Relay(relay.to_owned())))
.collect(); .collect();
// Set updating state // Set updating state
+1 -1
View File
@@ -111,7 +111,7 @@ impl RelayListPanel {
.author(public_key) .author(public_key)
.limit(1); .limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = client.database().query(filter).await?.into_iter().next() {
Ok(nip65::extract_relay_list(&event).collect()) Ok(nip65::extract_relay_list(&event).collect())
} else { } else {
Err(anyhow!("Not found.")) Err(anyhow!("Not found."))
+14 -1
View File
@@ -158,7 +158,20 @@ impl Sidebar {
}; };
let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move { let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move {
let contacts = client.database().contacts_public_keys(public_key).await?; let filter = Filter::new()
.author(public_key)
.kind(Kind::ContactList)
.limit(1);
let contacts: HashSet<PublicKey> = client
.database()
.query(filter)
.await?
.into_iter()
.next()
.map(|event| event.tags.public_keys().collect())
.unwrap_or_default();
Ok(contacts) Ok(contacts)
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ product-name = "Coop"
description = "Chat Freely, Stay Private on Nostr" description = "Chat Freely, Stay Private on Nostr"
identifier = "su.reya.coop" identifier = "su.reya.coop"
category = "SocialNetworking" category = "SocialNetworking"
version = "1.0.0-beta5" version = "1.0.1"
out-dir = "../dist" out-dir = "../dist"
before-packaging-command = "cargo build --release" before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"] resources = ["Cargo.toml", "src"]
-51
View File
@@ -1,51 +0,0 @@
[package]
name = "coop"
version.workspace = true
edition.workspace = true
publish.workspace = true
[[bin]]
name = "coop"
path = "src/main.rs"
[package.metadata.packager]
name = "Coop"
product-name = "Coop"
description = "Chat Freely, Stay Private on Nostr"
identifier = "su.reya.coop"
category = "SocialNetworking"
version = "1.0.0-beta5"
out-dir = "../dist"
before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"]
icons = [
"resources/32x32.png",
"resources/128x128.png",
"resources/128x128@2x.png",
"resources/icon.icns",
"resources/icon.ico",
]
[dependencies]
assets = { path = "../crates/assets" }
workspace = { path = "../crates/workspace" }
ui = { path = "../crates/ui" }
theme = { path = "../crates/theme" }
common = { path = "../crates/common" }
state = { path = "../crates/state" }
device = { path = "../crates/device" }
chat = { path = "../crates/chat" }
settings = { path = "../crates/settings" }
auto_update = { path = "../crates/auto_update" }
person = { path = "../crates/person" }
gpui.workspace = true
gpui_platform.workspace = true
gpui_linux.workspace = true
gpui_windows.workspace = true
gpui_macos.workspace = true
reqwest_client.workspace = true
log.workspace = true
tracing-subscriber.workspace = true
nostr-sdk.workspace = true
+4
View File
@@ -1,3 +1,7 @@
# Snaps built by snapcraft without Snap Store credentials are unsigned and
# cannot be installed without bypassing signature checks. Use:
# sudo snap install --dangerous ./coop_<version>_<arch>.snap
# For signed installs (`snap install coop`), publish via the Snap Store.
name: coop name: coop
title: Coop title: Coop
base: core24 base: core24
+4 -4
View File
@@ -55,8 +55,8 @@ flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo
Ensure you have: Ensure you have:
- [ ] Committed all changes - [ ] Committed all changes
- [ ] Tagged the release: `git tag -a v1.0.0-beta2 -m "Release v1.0.0-beta2"` - [ ] Tagged the release: `git tag -a v1.0.0 -m "Release v1.0.0"`
- [ ] Pushed the tag: `git push origin v1.0.0-beta2` - [ ] Pushed the tag: `git push origin v1.0.0`
- [ ] Run `./script/prepare-flathub.sh` to regenerate files - [ ] Run `./script/prepare-flathub.sh` to regenerate files
### 2. Fork and Submit ### 2. Fork and Submit
@@ -101,8 +101,8 @@ git push origin su.reya.coop
To release a new version: To release a new version:
1. Update version in workspace `Cargo.toml` 1. Update version in workspace `Cargo.toml`
2. Tag the new release: `git tag -a v1.0.0-beta3 -m "Release v1.0.0-beta3"` 2. Tag the new release: `git tag -a v1.0.0 -m "Release v1.0.0"`
3. Push the tag: `git push origin v1.0.0-beta3` 3. Push the tag: `git push origin v1.0.0`
4. Run `./script/prepare-flathub.sh` to regenerate 4. Run `./script/prepare-flathub.sh` to regenerate
5. Clone the flathub repo: `git clone https://github.com/flathub/su.reya.coop.git` 5. Clone the flathub repo: `git clone https://github.com/flathub/su.reya.coop.git`
6. Update the manifest with new commit/tag and hashes 6. Update the manifest with new commit/tag and hashes
+4
View File
@@ -35,3 +35,7 @@ SNAP_NAME="coop_${1}_${ARCH_SUFFIX}.snap"
snapcraft --destructive-mode --output "$SNAP_NAME" snapcraft --destructive-mode --output "$SNAP_NAME"
echo "Created snap package: $SNAP_NAME" echo "Created snap package: $SNAP_NAME"
echo ""
echo "This snap is unsigned (built without Snap Store credentials)."
echo "To install it locally, use:"
echo " sudo snap install --dangerous ./$SNAP_NAME"
+16 -10
View File
@@ -29,23 +29,29 @@ fi
# Function to update version in a Cargo.toml file # Function to update version in a Cargo.toml file
update_version() { update_version() {
local file="$1" local file="$1"
local backup="${file}.bak" local tmp="${file}.tmp"
# Backup the original file # Portable in-place edit. `sed -i` behaves differently on GNU sed (Linux)
cp "$file" "$backup" # and BSD sed (macOS): on macOS `sed -i -E` treats `-E` as the backup
# suffix instead of the extended-regex flag, leaving a stray
# More flexible regex that handles various version formats and whitespace # `Cargo.toml-E` behind and never updating the file.
if sed -i -E "s/^[[:space:]]*version[[:space:]]*=[[:space:]]*\"[^\"]+\"/version = \"$NEW_VERSION\"/" "$file"; then # Writing to a temp file and moving it over works on both implementations.
if sed -E "s/^[[:space:]]*version[[:space:]]*=[[:space:]]*\"[^\"]+\"/version = \"$NEW_VERSION\"/" "$file" > "$tmp" \
&& mv "$tmp" "$file"; then
echo "✓ Updated version to $NEW_VERSION in $file" echo "✓ Updated version to $NEW_VERSION in $file"
else else
echo "Error: Failed to update version in $file" echo "Error: Failed to update version in $file"
# Restore original backup # Remove any partial temp file; the original file is untouched
mv "$backup" "$file" rm -f "$tmp"
exit 1 exit 1
fi fi
# Remove the backup file # The substitution can silently match nothing (e.g. a `version.workspace` key),
rm -f "$backup" # so verify the new version actually landed before moving on.
if ! grep -q "^[[:space:]]*version[[:space:]]*=[[:space:]]*\"$NEW_VERSION\"" "$file"; then
echo "Error: Version line not found/updated in $file"
exit 1
fi
} }
# Update both Cargo.toml files # Update both Cargo.toml files
+16 -1
View File
@@ -15,11 +15,26 @@ if [ "$#" -ne 1 ]; then
exit 1 exit 1
fi fi
# Get system architecture (same mapping as script/bundle-snap)
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH_SUFFIX="x86_64" ;;
aarch64) ARCH_SUFFIX="aarch64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
snap_file="coop_${1}_${ARCH_SUFFIX}.snap"
if [ ! -f "$snap_file" ]; then
echo "Snap file not found: $snap_file"
echo "Build it first with: script/bundle-snap $1"
exit 1
fi
# Rerun as root # Rerun as root
[ "$UID" -eq 0 ] || exec sudo bash -e "$0" "$@" [ "$UID" -eq 0 ] || exec sudo bash -e "$0" "$@"
snap remove coop || true snap remove coop || true
mkdir -p snap mkdir -p snap
rm -rf snap/unpacked rm -rf snap/unpacked
unsquashfs -dest snap/unpacked "coop_$1_amd64.snap" unsquashfs -dest snap/unpacked "$snap_file"
snap try --classic snap/unpacked snap try --classic snap/unpacked