Merge origin/main: Navis 2.7.2+2 with app icon and update UX.

Combine remote update check/skip flow with green N AppIcon packaging for macOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-07-29 18:08:01 +03:00
co-authored by Cursor
35 changed files with 255 additions and 264 deletions
+52 -25
View File
@@ -16,9 +16,27 @@ import (
"time"
)
// CurrentVersion is the product/semver used for update eligibility (feed "version").
// Keep major.minor.patch only — no build suffix here.
const CurrentVersion = "2.7.2"
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
const BuildNumber = 2
// DefaultManifestURL is the update feed (hosted in the project git repo).
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
// DisplayVersion is shown in the UI (product + build), e.g. "2.7.2+1".
func DisplayVersion() string {
return fmt.Sprintf("%s+%d", CurrentVersion, BuildNumber)
}
// FileVersionDots is Major.Minor.Patch.Build for Windows resources / CFBundleVersion.
func FileVersionDots() string {
return fmt.Sprintf("%s.%d", CurrentVersion, BuildNumber)
}
// PlatformAsset is one OS/arch binary in the feed.
type PlatformAsset struct {
URL string `json:"url"`
@@ -87,7 +105,7 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
manifestURL = DefaultManifestURL
}
key := PlatformKey()
st := Status{Current: CurrentVersion, ManifestURL: manifestURL, Platform: key}
st := Status{Current: DisplayVersion(), ManifestURL: manifestURL, Platform: key}
m, err := fetchManifest(ctx, manifestURL)
if err != nil {
@@ -106,38 +124,46 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
}
st.URL = url
st.SHA256 = sha
// Product-only compare: build numbers must not keep offering an update after install
// when feed version equals CurrentVersion (e.g. 2.7.2 vs local 2.7.2+1).
st.Available = Compare(st.Latest, CurrentVersion) > 0 && url != ""
return st, nil
}
// Compare returns 1 if a>b, -1 if a<b, 0 if equal (semver-ish dotted ints).
// Compare returns 1 if a>b, -1 if a<b, 0 if equal.
//
// Update rule: only product semver (major.minor.patch) counts. Build metadata
// ("+N", "-suffix", or a 4th dotted component) is ignored so a just-updated app
// on the same product version is never treated as outdated vs the same feed.
func Compare(a, b string) int {
as := splitVer(a)
bs := splitVer(b)
n := len(as)
if len(bs) > n {
n = len(bs)
}
for i := 0; i < n; i++ {
var ai, bi int
if i < len(as) {
ai = as[i]
}
if i < len(bs) {
bi = bs[i]
}
if ai > bi {
as := productParts(a)
bs := productParts(b)
for i := 0; i < 3; i++ {
if as[i] > bs[i] {
return 1
}
if ai < bi {
if as[i] < bs[i] {
return -1
}
}
return 0
}
func productParts(v string) [3]int {
parts := splitVer(v)
var out [3]int
for i := 0; i < 3 && i < len(parts); i++ {
out[i] = parts[i]
}
return out
}
func splitVer(v string) []int {
v = strings.TrimPrefix(strings.TrimSpace(v), "v")
// Strip build metadata: 2.7.2+1 / 2.7.2-beta → compare product only.
if i := strings.IndexAny(v, "+-"); i >= 0 {
v = v[:i]
}
parts := strings.Split(v, ".")
out := make([]int, 0, len(parts))
for _, p := range parts {
@@ -146,9 +172,6 @@ func splitVer(v string) []int {
out = append(out, 0)
continue
}
if i := strings.IndexAny(p, "-+"); i >= 0 {
p = p[:i]
}
n, _ := strconv.Atoi(p)
out = append(out, n)
}
@@ -169,7 +192,7 @@ func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
if err != nil {
return Manifest{}, err
}
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
@@ -192,7 +215,7 @@ func downloadFile(ctx context.Context, url, dest string) error {
if err != nil {
return err
}
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
@@ -224,7 +247,8 @@ func fileSHA256(path string) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}
// prepareDownload fetches the update next to the running binary as "<exe>.new".
// prepareDownload fetches the update next to the running binary as temp download.
// Refuses when already on/newer than the feed product version (no re-apply loop).
func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha, exePath, tmpPath string, err error) {
st, err := Check(ctx, manifestURL)
if err != nil {
@@ -234,7 +258,7 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
if st.Error != "" {
return "", "", "", "", "", fmt.Errorf("%s", st.Error)
}
return "", "", "", "", "", fmt.Errorf("обновление не найдено")
return "", "", "", "", "", fmt.Errorf("уже актуальная версия %s (канал %s)", DisplayVersion(), st.Latest)
}
if !allowedDownloadURL(st.URL) {
return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
@@ -247,6 +271,9 @@ func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha,
if err != nil {
return "", "", "", "", "", err
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil && resolved != "" {
exe = resolved
}
tmp := filepath.Join(filepath.Dir(exe), ".navis-update-download")
_ = os.Remove(tmp)
// Also clear legacy leftover from older updaters.
+13
View File
@@ -15,6 +15,19 @@ func TestCompare(t *testing.T) {
if Compare("1.1.0", "1.1.0") != 0 {
t.Fatal("expected equal")
}
// Build must not make same product look outdated or newer for update checks.
if Compare("2.7.2", "2.7.2+9") != 0 {
t.Fatal("expected equal ignoring +build")
}
if Compare("2.7.2", "2.7.2.99") != 0 {
t.Fatal("expected equal ignoring 4th component")
}
if Compare("2.7.3", "2.7.2+1") <= 0 {
t.Fatal("expected product bump to win")
}
if DisplayVersion() == "" || !strings.Contains(DisplayVersion(), "+") {
t.Fatal("DisplayVersion should include build")
}
}
func TestManifestAssetFor(t *testing.T) {
-31
View File
@@ -1,31 +0,0 @@
package update
import "strings"
// BaseVersion is the marketing / release version (MAJOR.MINOR.PATCH).
const BaseVersion = "2.7.2"
// BuildNumber is the incremental build counter.
// Override at link time:
//
// -ldflags "-X vpnclient/internal/update.BuildNumber=42"
//
// Source default is bumped by build scripts via buildnum.txt.
var BuildNumber = "4"
// CurrentVersion is BaseVersion.BuildNumber, e.g. "2.7.2.1".
// Set in init so -ldflags BuildNumber is applied first.
var CurrentVersion string
func init() {
CurrentVersion = FullVersion()
}
// FullVersion returns "MAJOR.MINOR.PATCH.BUILD".
func FullVersion() string {
b := strings.TrimSpace(BuildNumber)
if b == "" {
b = "0"
}
return BaseVersion + "." + b
}